Skip to main content

Command Palette

Search for a command to run...

Building SupraDesk — A Fullstack Supabase Project with Next.js & TypeScript

Published
8 min read
S

Hi, I’m Saurabh Singh Rajput, a 2nd-year CSE student at IIIT Bhagalpur, passionate about building scalable web apps and exploring full-stack development.

Master Auth, Database, Storage, RLS, Realtime & Edge Functions in one real app.


Why this project?

You’ll learn:

  • Supabase Auth (email, magic link)

  • Sessions & middleware in Next.js

  • Postgres schema design & CRUD

  • Row Level Security (RLS)

  • Supabase Storage (private/public buckets, signed URLs)

  • Realtime channels & presence

  • Edge functions for server-side logic

  • Deployment & CI integration


Project overview (what you’ll build)

Levels:

  1. Auth & Protected Routes

  2. Profile system (DB + RLS)

  3. Notes (CRUD & RLS)

  4. Storage (file uploads & signed URLs)

  5. Blogging (publish, comments)

  6. Realtime (live updates & presence)

  7. Edge Functions (server logic)

  8. Admin dashboard & analytics


Quick Tech stack

  • Next.js (App Router) + TypeScript

  • Supabase (Auth, Postgres, Storage, Realtime, Edge Functions)

  • shadcn/ui (optional) for components

  • Tailwind CSS

  • Vercel (or Supabase hosting)


Quick code snippets (patterns you’ll reuse)

Init Supabase clients (client & server):

// lib/supabase-browser.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);

// lib/supabase-server.ts
import { createServerClient } from '@supabase/auth-helpers-nextjs';
export const serverSupabase = (req, res) => createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!, { req, res });

Protect route with middleware (example):

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
  const token = req.cookies['sb:token'];
  if (!token && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}

Upload file to storage (client):

const { data, error } = await supabase.storage.from('user-uploads').upload(`${userId}/${file.name}`, file);
const publicUrl = supabase.storage.from('user-uploads').getPublicUrl(data.path);

Realtime subscribe to a channel:

const channel = supabase.channel('notes:room')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'notes' }, payload => {
    // handle change
  })
  .subscribe();

Final notes

Start Level 1 and don’t skip RLS — it’s the single most critical concept for secure apps. I’ve given a full blueprint below (folders, files, SQL, policies). If you want, I’ll produce starter code for Level 1 (auth + middleware + profile) next.

Happy building — ping when you want Level 1 code. 🔥


📁 Full Project Blueprint — Folder Tree & File Roles

Below is the recommended repo structure followed by descriptions for each important file. This is not full code; it's a complete map you can implement.

supra-desk/
├─ .env                      # local env (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_KEY)
├─ package.json
├─ next.config.js
├─ tailwind.config.js
├─ tsconfig.json
├─ supabase/
│   └─ functions/             # Edge functions (Deno)
│       ├─ generate-slug/
│       ├─ notify-comment/
│       └─ cleanup-temp-files/
├─ prisma/ (optional)         # if using prisma migration for local dev
│
└─ src/
   ├─ app/                    # Next.js App Router
   │  ├─ layout.tsx
   │  ├─ page.tsx             # landing
   │  ├─ login/page.tsx
   │  ├─ signup/page.tsx
   │  ├─ dashboard/
   │  │   ├─ page.tsx         # dashboard landing
   │  │   ├─ notes/
   │  │   │   ├─ page.tsx     # notes list
   │  │   │   ├─ new/page.tsx # create
   │  │   │   └─ [id]/page.tsx# view/edit
   │  │   ├─ files/
   │  │   │   ├─ page.tsx     # file list & uploader
   │  │   │   └─ upload/page.tsx
   │  │   └─ blogs/
   │  │       ├─ page.tsx
   │  │       ├─ new/page.tsx
   │  │       └─ [id]/page.tsx
   │  └─ profile/
   │      ├─ page.tsx
   │      └─ edit/page.tsx
   │
   ├─ components/
   │  ├─ ui/                   # design system (shadcn wrappers)
   │  ├─ NoteCard.tsx
   │  ├─ FileUploader.tsx
   │  ├─ AvatarUpload.tsx
   │  └─ MarkdownEditor.tsx
   │
   ├─ lib/
   │  ├─ supabase-browser.ts   # client supabase
   │  ├─ supabase-server.ts    # server supabase (service key)
   │  ├─ db.ts                 # DB helpers, typed queries
   │  └─ realtime.ts
   │
   ├─ styles/
   ├─ hooks/
   │  ├─ useUser.ts
   │  └─ useRealtimeNotes.ts
   │
   ├─ utils/
   │  └─ slugify.ts
   │
   └─ middleware.ts            # protect dashboard routes

🔧 File-Level Details & Responsibilities

src/lib/supabase-browser.ts

  • Create Supabase client for browser.

  • Export supabase used in client components (auth, storage uploads, realtime).

src/lib/supabase-server.ts

  • Create server-side Supabase client (service key). Use for SSR or server actions that need elevated access.

  • Example for server calls in Next.js route handlers or API routes.

src/app/middleware.ts

  • Intercepts requests, checks user session cookie, redirect unauthenticated users from protected routes to /login.

  • Use Supabase cookie session or JWT.

src/app/login/page.tsx & signup/page.tsx

  • Forms for login and signup.

  • Use Supabase auth methods:

    • supabase.auth.signUp({ email, password })

    • supabase.auth.signInWithOtp({ email }) for magic link

    • supabase.auth.signInWithPassword(...)

src/app/dashboard/notes/*

  • Standard CRUD UIs. Use server/client fetching where appropriate.

  • Use supabase.auth.getSession() for server or supabase.auth.getUser() on client.

src/components/FileUploader.tsx

  • Handles file selection, progress, and upload to storage.

  • Use supabase.storage.from(bucket).upload(path, file).

src/supabase/functions/*

  • Edge functions: small Deno scripts to run server-side logic on events or via HTTP.

🧾 SQL Schema (Full) — Core Tables

Run these in Supabase SQL editor (create tables + primary keys + foreign keys).

-- users are managed by Supabase Auth, create profiles table
create table profiles (
  id uuid primary key references auth.users (id) on delete cascade,
  username text unique,
  full_name text,
  avatar_url text,
  bio text,
  created_at timestamptz default now()
);

-- notes table
create table notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references profiles (id) on delete cascade,
  title text,
  content text,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- blogs
create table blogs (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references profiles (id),
  title text,
  slug text unique,
  content text,
  published boolean default false,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- comments
create table comments (
  id uuid primary key default gen_random_uuid(),
  blog_id uuid references blogs(id) on delete cascade,
  user_id uuid references profiles(id),
  content text,
  created_at timestamptz default now()
);

-- files metadata (optional: track uploads)
create table user_files (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references profiles(id),
  bucket text,
  path text,
  name text,
  mime text,
  size bigint,
  created_at timestamptz default now()
);

🔐 RLS Policies (Essential)

Enable RLS for each table and add policies:

profiles

alter table profiles enable row level security;

create policy "profiles_select_self" on profiles for select
  using (auth.uid() = id);

create policy "profiles_update_self" on profiles for update
  using (auth.uid() = id);

notes

alter table notes enable row level security;

create policy "notes_select_user" on notes for select
  using (auth.uid() = user_id::text); -- adjust if types differ

create policy "notes_insert_user" on notes for insert
  with check (auth.uid() = user_id::text);

create policy "notes_update_user" on notes for update
  using (auth.uid() = user_id::text);

create policy "notes_delete_user" on notes for delete
  using (auth.uid() = user_id::text);

blogs — allow public select for published posts, but restrict update/insert to owner:

alter table blogs enable row level security;

create policy "blogs_select_published_or_owner" on blogs for select
  using (published = true OR auth.uid() = user_id::text);

create policy "blogs_manage_own" on blogs for insert, update, delete
  using (auth.uid() = user_id::text)
  with check (auth.uid() = user_id::text);

storage

  • Use Supabase Storage ACLs. For private buckets, only allow owner access via signed URLs. (Supabase handles this; you can use metadata to enforce ownership.)

🔁 Realtime Setup Basics

  • Use Postgres replication or Supabase Realtime (configured by default).

  • Listen to postgres_changes events for tables: notes, comments.

  • Use channels to broadcast minimal payloads (id + action). Clients can fetch the detail when needed.


🧰 Dev workflow & tips

Local dev

  • .env.local:

      NEXT_PUBLIC_SUPABASE_URL=https://xyz.supabase.co
      NEXT_PUBLIC_SUPABASE_ANON_KEY=anonkey
      SUPABASE_SERVICE_KEY=service_role_key (dev only)
    
  • Use supabase start (if using CLI local dev) or work against a dev project in Supabase UI.

Migrations

  • Use SQL migrations in supabase/migrations/ or use Prisma for local dev + Supabase for prod.

  • Keep RLS policies as SQL migration files.

Edge Functions

  • Write Deno functions under supabase/functions/*, test locally with supabase functions serve, deploy with supabase functions deploy.

Deploy

  • Vercel for frontend (set env vars).

  • Supabase manages DB & storage.

  • CI: on PR, run pnpm build & run tests.


✅ Step-By-Step Implementation Plan (Level-by-Level tasks)

Level 1: Auth & middleware (1 day)

  • Create supabase clients

  • Build signup, login pages

  • Implement middleware.ts to protect /dashboard

  • Create useUser hook and SessionProvider in layout

Level 2: Profile (1 day)

  • Create profiles table + RLS

  • Create edit profile UI & Avatar upload flow

  • Save avatar in user-uploads bucket & store path in profiles.avatar_url

Level 3: Notes CRUD (2 days)

  • Create notes table + RLS

  • Build notes listing, create, edit, delete

  • Use server actions or API routes for secure operations

  • Add pagination, search

Level 4: Storage (1 day)

  • Configure buckets: user-uploads (private) + public-assets (public)

  • Build FileUploader component

  • Store metadata in user_files table

  • Implement signed url downloads

Level 5: Blogging & comments (2 days)

  • blogs, comments tables + policies

  • Editor (Markdown) + image embeds

  • Slug generation (edge func or server-side)

  • Publish/unpublish flow

Level 6: Realtime (1-2 days)

  • Subscribe to notes & comments changes

  • Presence: track online users in a presence table or Realtime presence

  • Add optimistic UI updates

Level 7: Edge Functions (1 day)

  • Implement generate-slug function

  • Implement notify-new-comment to call email service (via service role or external provider)

Level 8: Admin (1 day)

  • Create admin-only views, analytics queries

  • Restrict to admin role via is_admin flag and policies


🎯 Useful Developer Utilities (recommended)

  • zod for schema validation

  • react-query / SWR for caching

  • @supabase/auth-helpers-nextjs for session handling

  • shadcn/ui for components or Chakra/Headless UI

  • remark / react-markdown for blog rendering

  • prettier, eslint & husky for linting & commit hooks


📦 Example Environment variables

  • NEXT_PUBLIC_SUPABASE_URL

  • NEXT_PUBLIC_SUPABASE_ANON_KEY

  • SUPABASE_SERVICE_KEY (server only)

  • NEXT_PUBLIC_VERCEL_URL (for images/absolute paths)


✅ Testing + QA

  • Unit test business logic (slug generator, DB helpers)

  • Integration test for API routes

  • Manual QA of RLS by creating multiple user accounts and verifying access

  • Run staging deployment (Vercel preview) and check deployment logs


🔐 Security & Production Notes

  • Never expose service_role key to client; keep server-only.

  • RLS is the most important security mechanism — enforce it strictly.

  • For private files use signed URLs; don’t store files in public buckets unless intended.

  • Rate-limit critical edge functions.