LaunchPad

Architecture

LaunchPad's tech stack, request flow, database schema, and key design decisions.

Tech Stack

┌─────────────────────────────────────────────────────┐
│                    FRONTEND                          │
│  Next.js 16 (App Router) + TypeScript + Tailwind    │
│  shadcn/ui components (New York style, Zinc palette)│
│  @dnd-kit (drag-and-drop)                           │
├─────────────────────────────────────────────────────┤
│                    HOSTING                           │
│  Vercel (auto-deploy from main branch)              │
│  Edge Middleware for auth session management         │
├─────────────────────────────────────────────────────┤
│                    BACKEND                           │
│  Supabase (BaaS)                                    │
│  ├── PostgreSQL (database)                          │
│  ├── Auth (email/password + GitHub OAuth)            │
│  ├── Row Level Security (RLS)                        │
│  └── PostgREST (auto-generated REST API)            │
└─────────────────────────────────────────────────────┘

E2E Request Flow

User Opens the App

Browser → Vercel CDN → Next.js Middleware

                     Check auth cookie

                    ┌───────┴───────┐
                    │               │
              Has session      No session
                    │               │
              Serve page      Redirect to
              (SSR/SSG)        /login

Authentication (GitHub OAuth)

  1. User clicks "Continue with GitHub"
  2. Client calls supabase.auth.signInWithOAuth({ provider: 'github' })
  3. Supabase redirects to GitHub OAuth consent screen
  4. User authorizes — GitHub redirects to Supabase callback
  5. Supabase exchanges code for tokens, creates user in auth.users
  6. Database trigger (handle_new_user) auto-creates profile in public.profiles
  7. Supabase redirects to /auth/callback in the app
  8. /auth/callback exchanges code for session, sets cookies
  9. User lands on /projects (authenticated)

Loading Projects (Server Component)

Browser request → Vercel → Next.js Server Component

                           createClient() (server)
                           reads auth cookie

                           supabase.auth.getUser()
                           (validates JWT)

                           getProjects(supabase)
                           SELECT *, tasks(count) FROM projects

                           PostgreSQL applies RLS:
                           can_access_project(id, auth.uid())

                           Returns only user's projects

                           Server renders HTML + streams to client

Creating a Task (Client Component)

User clicks "+" → CreateTaskDialog opens
User fills form → clicks "Create Task"

               Client-side Supabase client
               (uses cookie-based auth)

               createTask(supabase, { title, status, priority, project_id })

               POST /rest/v1/tasks → Supabase PostgREST

               RLS check: can_access_project(project_id, auth.uid())

               Insert succeeds → returns new task

               Client calls router.refresh()
               Server re-renders with new data

Drag-and-Drop Task Movement

User drags task card → @dnd-kit DndContext

                    onDragEnd fires

               Optimistic update (setState)
               UI instantly moves card

               moveTask(supabase, taskId, newStatus, newPosition)
               PATCH /rest/v1/tasks?id=eq.xxx

               On success: router.refresh() (sync server state)
               On failure: revert optimistic update

Database Schema

┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│  auth.users  │     │    profiles       │     │   projects   │
│  (Supabase)  │────▶│  id (FK→users)   │◀────│  owner_id    │
│              │     │  username         │     │  name        │
│              │     │  full_name        │     │  description │
│              │     │  avatar_url       │     │  archived    │
└──────────────┘     └──────────────────┘     └──────┬───────┘
                              │                       │
                              │                       │
                     ┌────────┴────────┐     ┌────────┴────────┐
                     │ project_members │     │     tasks       │
                     │ project_id (FK) │     │  project_id (FK)│
                     │ user_id (FK)    │     │  title          │
                     │ role            │     │  status (enum)  │
                     └─────────────────┘     │  priority (enum)│
                                             │  assignee_id    │
                                             │  position       │
                                             │  due_date       │
                                             └─────────────────┘

Enums

TypeValues
task_statusbacklog, in_progress, review, done
task_prioritylow, medium, high, urgent
member_roleowner, admin, member, viewer

Row Level Security (RLS)

RLS ensures users only see their own data. Every query goes through PostgreSQL policies.

The Circular Recursion Problem

The naive approach causes infinite recursion:

  • Projects policy checks project_members → project_members policy checks projects → loop

Solution: Security definer functions that bypass RLS for internal checks:

CREATE FUNCTION can_access_project(p_project_id uuid, p_user_id uuid)
RETURNS boolean AS $$
  SELECT EXISTS (
    SELECT 1 FROM projects WHERE id = p_project_id AND owner_id = p_user_id
  )
  OR EXISTS (
    SELECT 1 FROM project_members
    WHERE project_id = p_project_id AND user_id = p_user_id
  );
$$ LANGUAGE sql SECURITY DEFINER;

Policy Summary

TableSELECTINSERTUPDATEDELETE
profilesEveryoneAnyone (for trigger)Own only
projectsOwner or memberOwnerOwnerOwner
tasksProject accessProject accessProject accessProject access
project_membersEveryoneProject ownerProject owner

File Structure

src/
├── app/
│   ├── layout.tsx              # Root layout
│   ├── page.tsx                # Root redirect
│   ├── (auth)/                 # Auth route group (no sidebar)
│   │   ├── login/page.tsx
│   │   └── signup/page.tsx
│   ├── (dashboard)/            # Dashboard route group (with sidebar)
│   │   ├── layout.tsx
│   │   ├── dashboard/page.tsx
│   │   └── projects/
│   │       ├── page.tsx        # Project list (SSR)
│   │       └── [id]/page.tsx   # Kanban board
│   └── auth/callback/route.ts  # OAuth callback
├── components/
│   ├── board/                  # Kanban board components
│   ├── layout/                 # Sidebar, header
│   ├── projects/               # Project list + create dialog
│   └── ui/                     # shadcn/ui primitives
├── lib/supabase/
│   ├── client.ts               # Browser Supabase client
│   ├── server.ts               # Server Supabase client
│   ├── middleware.ts            # Auth session refresh
│   └── queries.ts              # All database queries
└── types/
    └── database.ts             # TypeScript types + enums

Key Design Decisions

Server Components + Client Components

  • Server Components (pages): Fetch data on the server, no client JS for initial render
  • Client Components (board, forms): Interactive UI with optimistic updates
  • Pattern: Server fetches → passes data as props → Client hydrates

Optimistic Updates

All mutations update the UI immediately before the server responds:

  1. Update local state (instant feedback)
  2. Send request to Supabase
  3. On success: router.refresh() to sync server state
  4. On failure: revert local state

Supabase SSR uses cookies instead of localStorage:

  • Works with Server Components (cookies available on server)
  • Middleware refreshes expired tokens automatically
  • No flash of unauthenticated content

Why No Real-time (Yet)

Supabase supports real-time subscriptions, but for a solo-user tool:

  • router.refresh() after mutations is sufficient
  • Avoids WebSocket connection overhead
  • Can be added later for multi-user collaboration

Deployment

GitHub (main) → Vercel (auto-deploy) → Production

              Environment vars:
              NEXT_PUBLIC_SUPABASE_URL
              NEXT_PUBLIC_SUPABASE_ANON_KEY
  • Build: next build (Turbopack)
  • Runtime: Vercel Serverless Functions (Node.js)
  • CDN: Vercel Edge Network (static assets)

On this page