Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app-directory/supabase-nextjs/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": true,
"extends": "next/core-web-vitals"
}
42 changes: 42 additions & 0 deletions app-directory/supabase-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/
next-env.d.ts

# Production
build
dist

# Misc
.DS_Store
*.pem

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local ENV files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Turborepo
.turbo

# typescript
*.tsbuildinfo
64 changes: 64 additions & 0 deletions app-directory/supabase-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# supabase-nextjs example

This example shows how to insert and retrieve data from a Supabase (Postgres) database using Next.js. It uses the App Router and SSR patterns:

- Mutation logic lives in `app/action.ts` using Next.js Server Actions.
- Query logic lives in `app/queries.ts` and is called from server components.
- Supabase client configuration is under `lib/supabase/`.

To run this example locally you need a `.env.local` file with your Supabase project keys:

```env
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
```

Add your Supabase API keys there and then start the dev server.
Comment on lines +9 to +16
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README is missing critical information about setting up the database schema in Supabase. Users need to create the 'notes' table with the required columns (id, username, title, description, created_at) before running the application. Add a section explaining the required database setup, including the SQL schema or instructions for creating the table in the Supabase dashboard.

Copilot uses AI. Check for mistakes.

## Running this project locally

1. Install dependencies:

```bash
pnpm install
```

2. Create a `.env.local` file in the project root and add your Supabase keys (see above).

3. Start the development server:

```bash
pnpm dev
```

4. Open the app in your browser:

```text
http://localhost:3000
```

## How to Use

You can choose from one of the following two methods to use this repository:

### One-Click Deploy

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/examples/tree/main/app-directory/supabase-nextjs&project-name=supabase-nextjs&repository-name=supabase-nextjs)

### Clone and Deploy

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:

```bash
pnpm create next-app --example https://github.com/vercel/examples/tree/main/app-directory/supabase-nextjs
```

Next, run Next.js in development mode:

```bash
pnpm dev
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=edge-middleware-eap) ([Documentation](https://nextjs.org/docs/deployment)).
34 changes: 34 additions & 0 deletions app-directory/supabase-nextjs/app/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use server'

import { createSupabaseServer } from '../lib/supabase/server'

export async function createNote(formData: FormData) {
try {
const username = formData.get('username')
const title = formData.get('title')
const description = formData.get('description')

if (
typeof username !== 'string' ||
typeof title !== 'string' ||
typeof description !== 'string'
) {
throw new Error('Missing required fields')
}

const supabase = await createSupabaseServer()

const { error } = await supabase.from('notes').insert({
username,
title,
description,
Comment on lines +19 to +24
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation only checks if fields are present and are strings, but doesn't validate that they're non-empty or trim whitespace. This allows creating notes with only whitespace or empty strings. Consider adding validation to ensure fields are not empty after trimming, e.g., if (!username.trim() || !title.trim() || !description.trim()) { throw new Error('Fields cannot be empty') }

Suggested change
const supabase = await createSupabaseServer()
const { error } = await supabase.from('notes').insert({
username,
title,
description,
const trimmedUsername = username.trim()
const trimmedTitle = title.trim()
const trimmedDescription = description.trim()
if (!trimmedUsername || !trimmedTitle || !trimmedDescription) {
throw new Error('Fields cannot be empty')
}
const supabase = await createSupabaseServer()
const { error } = await supabase.from('notes').insert({
username: trimmedUsername,
title: trimmedTitle,
description: trimmedDescription,

Copilot uses AI. Check for mistakes.
})

if (error) {
throw new Error(error.message)
}
Comment on lines +21 to +29
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully inserting a note, the server action should call revalidatePath to refresh the page data so the new note appears immediately. Add the following import at the top: import { revalidatePath } from 'next/cache' and call revalidatePath('/') after the successful insert (line 29, before the catch block ends or within the try block after checking for errors). Without this, users will need to manually refresh the page to see their newly created note.

Copilot uses AI. Check for mistakes.
} catch (err: any) {
console.error('Error creating note:', err?.message ?? err)
throw err
}
}
18 changes: 18 additions & 0 deletions app-directory/supabase-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReactNode } from 'react'
import { Layout, getMetadata } from '@vercel/examples-ui'
import '@vercel/examples-ui/globals.css'

export const metadata = getMetadata({
title: 'supabase-nextjs',
description: 'supabase-nextjs',
})

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Layout path="app-directory/supabase-nextjs">{children}</Layout>
</body>
</html>
)
}
32 changes: 32 additions & 0 deletions app-directory/supabase-nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Page, Text, Code, Link } from '@vercel/examples-ui'
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports Code, Link.

Suggested change
import { Page, Text, Code, Link } from '@vercel/examples-ui'
import { Page, Text } from '@vercel/examples-ui'

Copilot uses AI. Check for mistakes.
import { fetchNotes } from './queries'
import NotesCard from '../components/NotesCard'
import CreateNotes from '../components/CreateNotes'
import { createNote } from './action'

export default async function Home() {
const notes = await fetchNotes()

return (
<Page className="flex flex-col gap-12">
<section className="flex flex-col gap-6">
<Text variant="h1">supabase-nextjs usage example</Text>
<Text>
This project demonstrates how to use Supabase with Next.js App Router
to build a simple notes web app. It uses server-side rendering for
reading data and Next.js Server Actions for inserting new notes into a
Supabase (Postgres) database.
</Text>
</section>

<section className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<Text variant="h2">Notes</Text>
<CreateNotes createNote={createNote} />
</div>
{Array.isArray(notes) &&
notes.map((note) => <NotesCard key={note.id} note={note} />)}
</section>
</Page>
)
}
19 changes: 19 additions & 0 deletions app-directory/supabase-nextjs/app/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createSupabaseServer } from '../lib/supabase/server'

export async function fetchNotes() {
try {
const supabase = await createSupabaseServer()

const { data, error } = await supabase
.from('notes')
.select('*')
.order('created_at', { ascending: false }) // ascending: false, to show latest notes on top

if (error) throw new Error('Error while finding notes')
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in fetchNotes throws a generic error message "Error while finding notes" which loses the original Supabase error details. Consider throwing the original error or including the Supabase error message to help with debugging: throw new Error(error.message) instead of the generic message.

Suggested change
if (error) throw new Error('Error while finding notes')
if (error) throw new Error(error.message)

Copilot uses AI. Check for mistakes.

return data
} catch (err: any) {
console.error('Error fetching notes:', err?.message ?? err)
throw err
}
}
98 changes: 98 additions & 0 deletions app-directory/supabase-nextjs/components/CreateNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client'

import { useState, useTransition, type FormEvent } from 'react'

type CreateNotesProps = {
createNote: (formData: FormData) => Promise<void>
}

export default function CreateNotes({ createNote }: CreateNotesProps) {
const [open, setOpen] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isPending, startTransition] = useTransition()

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const form = event.currentTarget
const formData = new FormData(form)

setError(null)

startTransition(() => {
createNote(formData)
.then(() => {
form.reset()
setOpen(false)
})
.catch((err: any) => {
setError(err?.message ?? 'Something went wrong')
})
})
}

return (
<div className="relative">
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
className="inline-flex items-center rounded-md border border-zinc-800 bg-zinc-950 px-3 py-1.5 text-xs font-medium text-zinc-50 shadow-sm hover:border-zinc-700 hover:bg-zinc-900"
>
Create Note
</button>

{open && (
<div className="absolute right-0 top-full z-10 mt-3 w-80 sm:w-96 md:w-[28rem] rounded-xl border border-zinc-800 bg-zinc-950/95 p-4 shadow-xl ring-1 ring-white/10">
Comment on lines +43 to +44
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The popup form doesn't close when clicking outside of it, which is a common UX pattern users expect. Consider adding an onClick handler to a backdrop div or using a useEffect with a click-outside listener to close the form when users click outside the popup area.

Copilot uses AI. Check for mistakes.
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-medium text-zinc-50">New Note</h3>
<button
type="button"
onClick={() => setOpen(false)}
className="rounded-md p-1 text-xs text-zinc-400 hover:bg-zinc-900 hover:text-zinc-100"
>
Close
</button>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-3 text-xs">
<label className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-zinc-300">
Username
</span>
<input
name="username"
type="text"
className="h-8 rounded-md border border-zinc-800 bg-zinc-950 px-2 text-xs text-zinc-100 shadow-inner outline-none focus:border-zinc-500 focus:ring-1 focus:ring-zinc-500"
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-zinc-300">
Title
</span>
<input
name="title"
type="text"
className="h-8 rounded-md border border-zinc-800 bg-zinc-950 px-2 text-xs text-zinc-100 shadow-inner outline-none focus:border-zinc-500 focus:ring-1 focus:ring-zinc-500"
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-zinc-300">
Description
</span>
<textarea
name="description"
className="min-h-[80px] rounded-md border border-zinc-800 bg-zinc-950 px-2 py-1 text-xs text-zinc-100 shadow-inner outline-none focus:border-zinc-500 focus:ring-1 focus:ring-zinc-500"
/>
Comment on lines +60 to +86
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Form inputs are missing the required attribute. While validation exists on the server, adding required to the input elements (lines 60, 70, and 80) would provide better UX by triggering native browser validation before submission, preventing unnecessary server requests for empty fields.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

</label>
Comment on lines +56 to +87
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label elements are missing the htmlFor attribute which should match the input's id attribute for proper accessibility. This is important for screen readers and allows clicking the label to focus the input. Add id attributes to the inputs and corresponding htmlFor attributes to the labels, e.g., <label htmlFor="username" ...> and <input id="username" name="username" ...>.

Copilot uses AI. Check for mistakes.
{error && <p className="text-[11px] text-red-400">{error}</p>}
<button
type="submit"
disabled={isPending}
className="mt-4 inline-flex items-center justify-center rounded-md bg-zinc-900 px-3 py-1.5 text-xs font-medium text-zinc-50 shadow-sm ring-1 ring-zinc-700 hover:bg-zinc-800 hover:ring-zinc-600 disabled:opacity-60 disabled:hover:bg-zinc-900 disabled:hover:ring-zinc-700"
>
{isPending ? 'Saving…' : 'Submit'}
</button>
</form>
</div>
)}
</div>
)
}
32 changes: 32 additions & 0 deletions app-directory/supabase-nextjs/components/NotesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
type Note = {
id: string
username: string
title: string
description: string
created_at: string
}

type NotesCardProps = {
note: Note
}

export default function NotesCard({ note }: NotesCardProps) {
return (
<article className="mt-3 w-full max-w-xl rounded-xl border border-zinc-800 bg-zinc-950/80 p-4 shadow-sm ring-1 ring-white/5">
<header className="mb-2 flex items-center justify-between gap-3">
<h3 className="truncate text-sm font-medium text-zinc-50">
{note.title || 'Untitled Note'}
</h3>
<span className="shrink-0 text-[11px] text-zinc-500">
{new Date(note.created_at).toLocaleDateString()}
</span>
</header>
<div className="mb-2 text-[11px] text-zinc-400">
<span className="font-medium text-zinc-300">@{note.username}</span>
</div>
<p className="text-xs leading-relaxed text-zinc-200">
{note.description}
</p>
</article>
)
}
8 changes: 8 additions & 0 deletions app-directory/supabase-nextjs/lib/supabase/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createBrowserClient } from '@supabase/ssr'

export function createSupabaseBrowser() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
28 changes: 28 additions & 0 deletions app-directory/supabase-nextjs/lib/supabase/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'server-only'
import { cookies } from 'next/headers'
import { createServerClient } from '@supabase/ssr'

export async function createSupabaseServer() {
const cookieStore = await cookies()

return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll() // ✅ now this exists
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
} catch {
// In Server Components this can be read-only; middleware/route handlers handle refresh.
}
},
},
}
)
}
Loading
Loading