Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
88 changes: 88 additions & 0 deletions app-directory/supabase-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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
```

## Database schema

This example expects a `notes` table in your Supabase Postgres database. The table stores each note created from the UI:

| Column | Type | Description |
| ------------- | ------------- | ----------------------------------- |
| `id` | `uuid` | Primary key for the note |
| `username` | `text` | Name/handle of the note author |
| `title` | `text` | Short title for the note |
| `description` | `text` | Main content/body of the note |
| `created_at` | `timestamptz` | Timestamp when the note was created |

A possible SQL definition for this table is:

```sql
create table if not exists notes (
id uuid primary key default gen_random_uuid(),
username text not null,
title text not null,
description text,
created_at timestamptz not null default now()
);
```

## 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').trim()
const title = formData.get('title').trim()
const description = formData.get('description').trim()

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 } from '@vercel/examples-ui'
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.message)

return data
} catch (err: any) {
console.error('Error fetching notes:', err?.message ?? err)
throw err
}
}
101 changes: 101 additions & 0 deletions app-directory/supabase-nextjs/components/CreateNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'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"
required
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"
required
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"
required
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"
/>
</label>
{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!
)
}
Loading