Skip to content

Repository files navigation

Smart Bookmark App

A full-stack bookmark manager with real-time sync, built with Next.js 16 (App Router), Supabase (Auth + PostgreSQL + Realtime), and Tailwind CSS v4.

Live Demo: https://smart-bookmark-app-flame-nu.vercel.app


Features

  • Google OAuth — One-click sign-in via Google (no email/password)
  • Add & delete bookmarks — Save any URL with a custom title
  • Private by default — Row Level Security (RLS) ensures each user only sees their own bookmarks
  • Real-time sync — Changes propagate instantly across all open tabs using Supabase Realtime (Postgres Changes)
  • Responsive UI — Clean, mobile-friendly design with loading skeletons and micro-animations
  • Favicon previews — Automatically fetches site favicons for each bookmark

Tech Stack

Layer Technology
Framework Next.js 16 (App Router, React 19)
Auth Supabase Auth (Google OAuth provider)
Database Supabase PostgreSQL with Row Level Security
Real-time Supabase Realtime (Postgres Changes)
Styling Tailwind CSS v4
Language TypeScript
Deployment Vercel

Project Structure

src/
├── app/
│   ├── auth/
│   │   ├── callback/route.ts   # OAuth callback — exchanges code for session
│   │   └── signout/route.ts    # Server-side sign-out
│   ├── layout.tsx              # Root layout (Geist font, metadata)
│   ├── page.tsx                # Home page — hero (logged out) or bookmark manager (logged in)
│   ├── globals.css             # Tailwind + custom animations
│   └── favicon.ico
├── components/
│   ├── auth-button.tsx         # Google sign-in button / user avatar dropdown
│   ├── bookmark-manager.tsx    # Main bookmark list with real-time subscription
│   └── add-bookmark-form.tsx   # Form to add new bookmarks
├── lib/supabase/
│   ├── client.ts               # Browser Supabase client (createBrowserClient)
│   └── server.ts               # Server Supabase client (createServerClient)
└── middleware.ts                # Refreshes auth session on every request
supabase-schema.sql             # Database schema, RLS policies, and Realtime config

How It Works

  1. Authentication — Users sign in with Google OAuth via Supabase Auth. The OAuth callback (/auth/callback) exchanges the authorization code for a session. Middleware (middleware.ts) refreshes the auth token on every request to keep the session alive.

  2. Data flow — The home page (page.tsx) is a server component that checks the user's auth state. If logged in, it renders <BookmarkManager />, a client component that fetches bookmarks from Supabase and subscribes to real-time changes.

  3. Real-time updatesBookmarkManager opens a Supabase Realtime channel listening for INSERT and DELETE events on the bookmarks table. When another tab adds or removes a bookmark, the list updates instantly without polling.

  4. Security — Row Level Security policies on the bookmarks table ensure users can only SELECT, INSERT, and DELETE their own rows. The user_id column defaults to auth.uid(), so the client never needs to send the user ID.


Setup

1. Create a Supabase Project

  1. Go to supabase.com and create a new project.
  2. Open the SQL Editor and run the contents of supabase-schema.sql to create the bookmarks table, RLS policies, and enable Realtime.

2. Configure Google OAuth

  1. In the Supabase Dashboard, go to Authentication → Providers → Google and enable it.
  2. Create OAuth credentials at the Google Cloud Console:
    • Application type: Web application
    • Authorized redirect URI: https://<your-project-ref>.supabase.co/auth/v1/callback
  3. Copy the Client ID and Client Secret into the Supabase Google provider settings.

3. Set Environment Variables

Create a .env.local file in the project root:

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

You can find these values in your Supabase project under Settings → API.

4. Run Locally

npm install
npm run dev

Open http://localhost:3000.

5. Deploy to Vercel

  1. Push this repo to GitHub.
  2. Import it on vercel.com.
  3. Add the same environment variables (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY) in the Vercel project settings.
  4. Update the Google OAuth redirect URI to also allow your Vercel domain: https://your-app.vercel.app/auth/callback

Problems Encountered and Solutions

1. Supabase Realtime not receiving events

Problem: After setting up the Realtime subscription, no INSERT or DELETE events were being delivered to the client.

Root cause: Supabase Realtime only broadcasts changes for tables that are explicitly added to the supabase_realtime publication. By default, new tables are not included.

Solution: Added the table to the publication in the SQL schema:

ALTER PUBLICATION supabase_realtime ADD TABLE public.bookmarks;

2. Auth session not persisting on page refresh

Problem: After signing in with Google, the user was logged out on every page refresh or navigation.

Root cause: The @supabase/ssr package stores the session in cookies, but the auth token expires and needs to be refreshed on each server request. Without middleware, the expired token was never renewed.

Solution: Created src/middleware.ts that intercepts all non-static routes and calls supabase.auth.getUser() to refresh the session cookie before the page renders.


3. RLS blocking inserts from the client

Problem: When a logged-in user tried to add a bookmark, the insert was rejected by Row Level Security even though the user was authenticated.

Root cause: The RLS INSERT policy uses WITH CHECK (auth.uid() = user_id), which requires the user_id to match the authenticated user. If the client doesn't send user_id, it defaults to NULL, which fails the check.

Solution: Set DEFAULT auth.uid() on the user_id column so PostgreSQL automatically fills it with the authenticated user's ID on insert — the client only needs to send url and title.


4. Duplicate bookmarks appearing in the real-time list

Problem: When a user added a bookmark, it appeared twice in the list — once from the local state update and again from the Realtime INSERT event.

Root cause: The Realtime subscription fires for all changes, including those made by the same client. Both the form submission callback and the Realtime handler were adding the new bookmark to state.

Solution: Added de-duplication in the Realtime handler by checking if a bookmark with the same id already exists before adding it:

setBookmarks((prev) => {
  if (prev.some((b) => b.id === payload.new.id)) return prev;
  return [payload.new as Bookmark, ...prev];
});

License

MIT

About

A full-stack bookmark manager with real-time sync, built with Next.js 16 (App Router), Supabase (Auth + PostgreSQL + Realtime), and Tailwind CSS v4.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages