A production-grade Jira clone with live collaboration powered by Firebase, React, and TypeScript.
- Live Presence Bar — animated avatar stack showing who's online right now (Firebase RTDB)
- Instant Sync — all card moves, edits, and task updates propagate to every connected user via Firestore
onSnapshot— no refresh needed - Concurrency Control — Firestore Transactions prevent data overwrites when two users edit simultaneously
- Optimistic UI — drag-and-drop feels instant; changes revert gracefully if the write fails
- Activity Feed — real-time log of every action (who moved what, when)
- Drag-and-drop kanban via
@hello-pangea/dnd - Add / rename / delete columns
- Create tasks with priority, assignees, Markdown description
- Task detail panel with inline editing, comments, status changes
- Priority tags: Critical 🔴, High 🟠, Medium 🟡, Low 🟢
- Google sign-in (one click)
- Email + password (register / sign in)
- Protected routes — unauthenticated users redirected to
/auth
src/
├── components/
│ ├── board/
│ │ ├── KanbanBoard.tsx # DragDropContext + optimistic updates
│ │ ├── KanbanColumn.tsx # Droppable column with task list
│ │ ├── TaskCard.tsx # Draggable task card
│ │ ├── PresenceBar.tsx # Live online users indicator
│ │ └── ActivityFeed.tsx # Real-time activity sidebar
│ ├── task/
│ │ ├── CreateTaskDialog.tsx # New task form
│ │ └── TaskDetailModal.tsx # Full task editor + comments
│ ├── layout/
│ │ └── ProtectedRoute.tsx
│ └── ui/ # Shadcn/Radix UI components
├── context/
│ ├── AuthContext.tsx # Firebase auth state
│ └── ToastContext.tsx # Global notifications
├── hooks/
│ ├── useProjectSync.ts # ★ Core: 4x onSnapshot listeners
│ ├── usePresence.ts # RTDB presence registration + subscription
│ ├── useProjects.ts # Real-time project list
│ └── useComments.ts # Real-time task comments
├── services/
│ ├── authService.ts # Google / email auth + user doc
│ ├── projectService.ts # Project + column CRUD
│ ├── taskService.ts # Task CRUD with Transactions + drag moves
│ └── presenceService.ts # RTDB presence helpers
├── pages/
│ ├── AuthPage.tsx
│ ├── ProjectsPage.tsx
│ └── BoardPage.tsx
├── types/
│ └── index.ts # All TypeScript interfaces
└── lib/
├── firebase.ts # Firebase initialization
└── utils.ts # cn(), getInitials(), PRIORITY_CONFIG, etc.
git clone <your-repo-url>
cd jira-clone
npm install- Go to console.firebase.google.com
- Click Add project → name it (e.g.
flowboard) - Disable Google Analytics (optional) → Create project
- Sidebar → Firestore Database → Create database
- Choose production mode (rules are provided)
- Select a region close to your users → Enable
- Sidebar → Realtime Database → Create database
- Choose a location → Start in locked mode
- Note your database URL (e.g.
https://your-project-default-rtdb.firebaseio.com)
- Sidebar → Authentication → Get started
- Sign-in method tab → enable:
- Email/Password → Enable → Save
- Google → Enable → add your Project support email → Save
- Project Overview → gear icon → Project settings
- Scroll to Your apps → click
</>(Web) - Register app (nickname:
flowboard-web) → don't need Firebase Hosting - Copy the
firebaseConfigobject values
cp .env.example .envEdit .env with your values:
VITE_FIREBASE_API_KEY=AIzaSy...
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=1:123456789:web:abc123
VITE_FIREBASE_MEASUREMENT_ID=G-XXXXXXX
VITE_FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
⚠️ Never commit.env— it's in.gitignore. Use Vercel's environment variable UI for production.
Install Firebase CLI if you haven't:
npm install -g firebase-tools
firebase login
firebase use --add # select your projectDeploy Firestore rules + indexes + RTDB rules:
firebase deploy --only firestore:rules,firestore:indexes,databasenpm run devnpm install -g vercel
vercelFollow the prompts. When asked about environment variables, add each VITE_FIREBASE_* variable.
- Push your code to GitHub
- Go to vercel.com → New Project → import your repo
- Framework preset: Vite (auto-detected)
- Environment Variables tab → add all
VITE_FIREBASE_*keys - Click Deploy
The
vercel.jsonrewrite rule ensures React Router works with direct URL navigation.
Add your Vercel domain to Firebase Auth's authorized domains:
- Firebase Console → Authentication → Settings → Authorized domains
- Click Add domain → paste your
*.vercel.appURL
useProjectSync(projectId)
├── onSnapshot(project doc) → project metadata
├── onSnapshot(columns subcol) → all columns
├── onSnapshot(tasks subcol) → all tasks (ordered by `order`)
└── onSnapshot(activity subcol) → activity log
All four listeners run in parallel. The hook tracks readiness and sets loading: false only when all three primary listeners have fired at least once.
All task moves and updates use runTransaction():
// moveTask — atomic drag-and-drop
await runTransaction(db, async (transaction) => {
const taskSnap = await transaction.get(taskRef); // Read inside transaction
if (!taskSnap.exists()) throw new Error('Task not found');
transaction.update(taskRef, { columnId: destColumnId, order: Date.now() });
transaction.update(srcColRef, { taskIds: newSrcIds });
transaction.update(dstColRef, { taskIds: newDstIds });
});If two users drag the same card simultaneously, the second transaction will retry with fresh data.
User drags card →
1. onOptimisticUpdate() patches local BoardState instantly (0ms)
2. moveTask() transaction runs against Firestore (~200–500ms)
3. onSnapshot fires with confirmed server state
4. Optimistic patch clears, Firestore state takes over
On error:
3. Catch block reverts optimistic state
4. Toast notification shown
registerPresence(uid, projectId)
├── set(rtdb, presence/{projectId}/{uid}) ← RTDB write
├── onDisconnect(presenceRef).remove() ← Auto-cleanup on disconnect
└── setDoc(firestore, .../presence/{uid}) ← Firestore mirror
subscribeToPresence(projectId, callback)
└── onValue(rtdb, presence/{projectId}) ← Real-time listener
| Resource | Read | Create | Update | Delete |
|---|---|---|---|---|
/users/{uid} |
Any auth user | Own user only | Own user only | ✗ |
/projects/{id} |
Members only | Auth user (becomes owner) | Members only | Owner only |
/projects/{id}/tasks/{id} |
Members only | Members (sets self as reporter) | Members only | Members only |
/projects/{id}/activity/{id} |
Members only | Members (sets self as userId) | ✗ | ✗ |
RTDB /presence/{proj}/{uid} |
Any auth user | Own uid only | Own uid only | Own uid only |
| Layer | Technology |
|---|---|
| Frontend framework | React 18 + Vite |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 3 |
| UI components | Radix UI + Shadcn patterns |
| Drag & Drop | @hello-pangea/dnd |
| Markdown | react-markdown |
| Real-time DB | Firebase Firestore 10 |
| Presence | Firebase Realtime Database |
| Auth | Firebase Auth (Google + Email) |
| Offline support | Firestore persistent multi-tab cache |
| Routing | React Router 6 |
| Deployment | Vercel |
npm run dev # Start development server (localhost:5173)
npm run build # TypeScript check + Vite production build
npm run preview # Preview production build locally
npm run lint # ESLint check"Missing or insufficient permissions"
→ Deploy Firestore rules: firebase deploy --only firestore:rules
→ Ensure the user is a member of the project
Google sign-in popup blocked → Allow popups for localhost in your browser settings
Presence not showing
→ Check RTDB rules are deployed: firebase deploy --only database
→ Confirm VITE_FIREBASE_DATABASE_URL is set correctly
Tasks not updating in real-time
→ Check browser console for Firestore listener errors
→ Ensure Firestore indexes are deployed: firebase deploy --only firestore:indexes
"auth/unauthorized-domain" on production → Add your Vercel domain in Firebase Console → Authentication → Settings → Authorized domains
This project uses a hybrid database approach: Cloud Firestore for persistent data and Firebase Realtime Database (RTDB) for live user presence (online/offline status).
The Realtime Database requires a specific URL. Because our database is hosted in Singapore, use this format:
VITE_FIREBASE_DATABASE_URL="https://YOUR_PROJECT_ID.asia-southeast1.firebasedatabase.app"Navigate to Realtime Database > Rules and publish the following. This allows users to see who is online while only being able to edit their own status:
{
"rules": {
"presence": {
"$projectId": {
".read": "auth != null",
"$uid": {
".write": "auth != null && auth.uid === $uid"
}
}
}
}
}
Firestore does not detect "unexpected" disconnects (tab closing/internet loss). RTDB uses the onDisconnect() hook to automatically remove user records, keeping our "Online" list 100% accurate.


