-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Supabase + Next.js Notes App #1387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
309868b
ae7313e
ea2184a
7c3f570
4acd35d
c334f87
49fb1e5
2051b2d
485fa51
e4545be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "root": true, | ||
| "extends": "next/core-web-vitals" | ||
| } |
| 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 |
| 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. | ||
|
|
||
| ## 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): | ||
|
|
||
| [](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)). | ||
| 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
|
||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Jan 23, 2026
There was a problem hiding this comment.
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.
| 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> | ||
| ) | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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 | ||
| } | ||
| } |
| 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
|
||
| <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> | ||
| ) | ||
| } | ||
| 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> | ||
| ) | ||
| } |
| 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! | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.