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
- 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
| 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 |
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
-
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. -
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. -
Real-time updates —
BookmarkManageropens a Supabase Realtime channel listening forINSERTandDELETEevents on thebookmarkstable. When another tab adds or removes a bookmark, the list updates instantly without polling. -
Security — Row Level Security policies on the
bookmarkstable ensure users can onlySELECT,INSERT, andDELETEtheir own rows. Theuser_idcolumn defaults toauth.uid(), so the client never needs to send the user ID.
- Go to supabase.com and create a new project.
- Open the SQL Editor and run the contents of
supabase-schema.sqlto create thebookmarkstable, RLS policies, and enable Realtime.
- In the Supabase Dashboard, go to Authentication → Providers → Google and enable it.
- Create OAuth credentials at the Google Cloud Console:
- Application type: Web application
- Authorized redirect URI:
https://<your-project-ref>.supabase.co/auth/v1/callback
- Copy the Client ID and Client Secret into the Supabase Google provider settings.
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-keyYou can find these values in your Supabase project under Settings → API.
npm install
npm run devOpen http://localhost:3000.
- Push this repo to GitHub.
- Import it on vercel.com.
- Add the same environment variables (
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY) in the Vercel project settings. - Update the Google OAuth redirect URI to also allow your Vercel domain:
https://your-app.vercel.app/auth/callback
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;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.
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.
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];
});MIT