A full-featured video conferencing web application built with Next.js, TypeScript, Clerk for authentication, and Stream for real-time video communication. LinkUp lets users instantly start meetings, schedule future calls, join via invite links, and revisit recordings — all from a clean, dark-themed dashboard.
- Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Environment Variables
- Key Design Decisions
- Scripts
- Instant Meetings — Start a video call with a single click
- Scheduled Meetings — Pick a future date and time; share the invite link before the call
- Join via Link — Paste any meeting link to join a call
- Personal Room — Every user gets a persistent room tied to their account
- Device Setup — Camera and microphone check before entering any meeting
- Flexible Layouts — Switch between Grid, Speaker-Left, and Speaker-Right views mid-call
- Participant List — See everyone in the room with name and avatar
- Call Statistics — Real-time network and quality metrics
- End Call (Owner) — Meeting host can end the call for all participants
- Recordings — Browse and replay past meeting recordings
- Upcoming & Previous Calls — Dashboard views for all your scheduled and ended meetings
- Responsive Design — Full mobile support with a slide-out navigation drawer
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Authentication | Clerk |
| Real-time Video | Stream Video React SDK |
| Stream Server SDK | @stream-io/node-sdk |
| Styling | Tailwind CSS |
| UI Components | shadcn/ui + Radix UI |
| Icons | Lucide React |
| Date Picking | react-datepicker |
| Date Utilities | date-fns |
| Class Utilities | clsx + tailwind-merge |
The diagram below shows how the major layers of LinkUp interact — from the browser through Next.js middleware, authentication, video infra, and back.
graph TB
subgraph Client["Browser / Client"]
UI["React Components<br/>(Next.js App Router)"]
SW["Stream Video SDK<br/>(@stream-io/video-react-sdk)"]
end
subgraph NextJS["Next.js Server"]
MW["Clerk Middleware<br/>(Route Protection)"]
SA["Server Action<br/>stream.actions.ts<br/>(Token Generation)"]
Pages["Pages & Layouts<br/>app/(root)/**"]
end
subgraph Auth["Clerk Platform"]
ClerkAPI["Clerk Auth API"]
Session["User Session / JWT"]
end
subgraph StreamPlatform["Stream Platform"]
StreamAPI["Stream REST API"]
WebRTC["WebRTC Media Server<br/>(TURN / SFU)"]
Recordings["Recording Storage"]
end
User(("User")) -->|HTTP Request| MW
MW -->|Authenticated?| ClerkAPI
ClerkAPI -->|Session Token| Session
Session -->|Authorized| Pages
Pages -->|Render| UI
UI -->|useUser / SignedIn| ClerkAPI
UI -->|Initialize Client| SW
SW -->|Request Token| SA
SA -->|CLERK_SECRET_KEY| ClerkAPI
SA -->|STREAM_SECRET_KEY<br/>Generate User Token| StreamAPI
StreamAPI -->|JWT Token| SA
SA -->|Token| SW
SW -->|Join / Create Call| StreamAPI
StreamAPI -->|Call Details| SW
SW <-->|Audio / Video / Signaling| WebRTC
StreamAPI -->|Store| Recordings
SW -->|Query Recordings| StreamAPI
style Client fill:#1c1f2e,color:#fff,stroke:#0E78F9
style NextJS fill:#252a41,color:#fff,stroke:#0E78F9
style Auth fill:#1a1a2e,color:#fff,stroke:#7c3aed
style StreamPlatform fill:#1a2e1a,color:#fff,stroke:#16a34a
LinkUp/
├── app/
│ ├── (auth)/ # Public auth routes
│ │ ├── sign-in/[[...sign-in]]/ # Clerk sign-in page
│ │ └── sign-up/[[...sign-up]]/ # Clerk sign-up page
│ ├── (root)/ # Protected routes (auth required)
│ │ ├── (home)/
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── upcoming/ # Upcoming meetings
│ │ │ ├── previous/ # Ended meetings
│ │ │ ├── recordings/ # Meeting recordings
│ │ │ └── personal-room/ # Personal room
│ │ ├── meeting/[id]/ # Active meeting room
│ │ └── layout.tsx # Wraps with StreamVideoProvider
│ ├── constants/index.ts # Nav links, avatar images
│ ├── globals.css # Global styles + Stream overrides
│ └── layout.tsx # Root layout (ClerkProvider)
│
├── components/
│ ├── ui/ # shadcn/ui primitives
│ ├── Navbar.tsx # Top navigation bar
│ ├── Sidebar.tsx # Left navigation sidebar
│ ├── MobileNav.tsx # Mobile drawer nav
│ ├── MeetingTypeList.tsx # Home page action cards + meeting logic
│ ├── HomeCard.tsx # Single action card
│ ├── MeetingModal.tsx # Reusable meeting dialog
│ ├── CallList.tsx # Upcoming / previous / recordings list
│ ├── MeetingCard.tsx # Single meeting or recording card
│ ├── MeetingSetup.tsx # Pre-meeting device check
│ ├── MeetingRoom.tsx # Active call UI with layout controls
│ ├── EndCallButton.tsx # Host-only end call button
│ └── Loader.tsx # Loading spinner
│
├── hooks/
│ ├── useGetCalls.ts # Fetch all user calls (upcoming/ended/recordings)
│ ├── useGetCallById.ts # Fetch a single call by ID
│ └── use-toast.ts # Toast notification hook
│
├── actions/
│ └── stream.actions.ts # Server action: Stream JWT token generation
│
├── poviders/
│ └── StreamClientProvidet.tsx # StreamVideoClient initialization provider
│
├── lib/
│ └── utils.ts # cn() classname utility
│
├── middleware.ts # Clerk route protection middleware
├── next.config.mjs
├── tailwind.config.ts
└── tsconfig.json
# 1. Clone the repository
git clone https://github.com/your-username/LinkUp.git
cd LinkUp
# 2. Install dependencies
npm install
# 3. Set up environment variables
cp .env.example .env.local
# Fill in all required keys (see Environment Variables section)
# 4. Run the development server
npm run devOpen http://localhost:3000 in your browser.
npm run build
npm startCreate a .env.local file in the project root:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
# Stream Video
NEXT_PUBLIC_STREAM_API_KEY=<your-stream-api-key>
STREAM_SECRET_KEY=<your-stream-secret-key>
# App
NEXT_PUBLIC_BASE_URL=http://localhost:3000Where to find these keys:
- Clerk → Clerk Dashboard → Your App → API Keys
- Stream → GetStream Dashboard → Your App → Overview
| Decision | Rationale |
|---|---|
| Next.js App Router | Server Components reduce client-side JS; route groups enable clean auth vs. protected layout separation |
| Clerk Middleware | Declarative route protection without manual redirect logic in every page |
| Stream Server Action for token | Keeps STREAM_SECRET_KEY server-side only — never exposed to the browser |
| shadcn/ui | Components live in the repo; fully customizable without fighting a library's override system |
StreamVideoProvider in protected layout |
Initializes the video client once at the layout level so all child pages share the same client instance |
| Command | Description |
|---|---|
npm run dev |
Start local development server |
npm run build |
Build for production |
npm start |
Start production server |
npm run lint |
Run ESLint |