Freemium team coordination hub. Async daily standups with automated email digests, live team boards & a prioritized GitHub PR review queue.
- Overview
- Problem & Solution
- Target Users
- Business Model
- Feature Scope
- Tech Stack
- System Architecture
- Database Schema
- API Routes
- Pages & Routes
- PR Review Queue Feature
- Digest Engine
- Real-time (WebSockets)
- Email System
- Payments (Stripe)
- Folder Structure
- Environment Variables
- 5-Week Build Plan
- DSA & CS Concepts Exercised
- Competitors
- How to Contribute.
| Field | Detail |
|---|---|
| Project name | Standup.dev |
| Type | Freemium SaaS. (Team coordination tool) |
| Core value | Replace daily standups & scattered PR review tracking with one clean async tool. |
| Primary stack | React (Vite) · Express · PostgreSQL · Redis · BullMQ · WebSockets · Stripe |
| Timeline | 6 weeks (MVP + clean code) |
| Marketing site | mystandup.in — landing page, pricing, product info |
| App | app.mystandup.in — the actual product, requires sign in |
| Deployment | Vercel (marketing + app frontend) · Railway (backend + Redis) · Neon (PostgreSQL) |
Remote dev teams have no lightweight, structured way to share daily progress without it turning into either:
- A 15-minute meeting that should have been a message
- A Slack bot that everyone ignores after day 3
- A Notion doc that goes stale immediately
- A manager chasing people manually
On top of that, PR reviews are a separate pain entirely.
There is no priority signal in GitHub's native interface. Developers have no idea which PRs are blocking teammates, which are stale & which need their eyes first.
The result is reviews getting chased over Slack and PRs sitting open for days.
Standup.dev is a team coordination hub with two core features:
- Async standups : Each developer fills 3 fields in 90 seconds. The tool compiles everyone's updates into one clean digest sent at a team-configured time.
- PR Review Queue : A prioritized, scored list of open PRs across the team's connected repos. Each developer sees exactly what needs their review & why.
Both features live in one place. One tool. One daily habit.
| User type | Pain point |
|---|---|
| Remote dev teams at small startups | Async coordination across timezones |
| Open source project contributors | No structured way to share progress across async contributors |
| Bootcamp cohorts & study groups | Group project coordination without heavy tooling |
| Solo developers building in public | Accountability and structure for themselves |
- Up to 5 members per team
- Unlimited standups and history
- Daily email digest
- PR Review Queue (up to 3 repos)
- Live team board
- Unlimited team members
- Unlimited repos in PR Review Queue
- Slack digest integration
- Weekly summary report for team leads
- Missed standup reminders via email
- Custom standup questions (beyond the default 3)
- Priority email support
- Geekbot charges $2.50/user/month — for a 5-person team that's $12.50/mo
- Standup.dev charges $9/team flat — simpler, cheaper, more predictable
- Free plan is generous enough to get real teams using it before converting
- Create a team workspace with a shareable invite link.
- Daily standup form, 3 fields: Done, Doing, Blocked.
- Set a digest time per team with timezone support.
- Automated daily email digest compiled from all member submissions.
- Live team board showing who has and hasn't submitted today.
- Blockers highlighted at the top of every digest.
- GitHub PR/issue URL linking. (Paste a URL, title is fetched automatically)
- PR Review Queue. Open PRs across connected repos, scored by urgency (up to 3 repos)
- Up to 5 members per team.
- Unlimited team members.
- Unlimited repos in PR Review Queue.
- Slack webhook. (Digest posted to a chosen channel automatically)
- Weekly summary report. (Participation rates, blocker trends)
- Missed standup reminder email. (Sent 1 hour before digest time)
- Custom questions. (Add fields beyond the default 3)
- Linear / Jira / GitHub Issues deep integration
- Analytics dashboard with historical participation trends
- Mobile app or PWA
- SSO / SAML for enterprise
- Public team page
- AI-generated weekly summaries
| Tool | Purpose |
|---|---|
| React 18 + Vite | Frontend framework for fast dev server, HMR, optimized builds |
| TypeScript | Type safety across the entire codebase |
| React Router v6 | Client-side routing. No file-based routing, full manual control |
| Tailwind CSS | Utility-first styling |
| shadcn/ui | Accessible component library built on Radix UI |
| TanStack Query | Server state management, caching, background refetching |
| Socket.io client | Real-time live board updates |
| Zod | Client-side form validation |
| React Hook Form | Form state management, pairs with Zod for validation |
| Tool | Purpose |
|---|---|
| Node.js + Express | REST API server |
| TypeScript | Type safety |
| Zod | Request body validation and schema definition |
| Passport.js + JWT | Authentication. (GitHub OAuth, Google OAuth, JWT session tokens) |
| bcryptjs | Password hashing (if email/password auth added later) |
| Prisma | Type-safe ORM for PostgreSQL |
| Tool | Purpose |
|---|---|
| PostgreSQL (Neon) | Primary relational database. (serverless, free tier) |
| Redis (Railway) | Caching GitHub API responses, BullMQ job store, rate limiting |
| BullMQ | Job queue for scheduled digests, reminder emails, PR polling |
| Socket.io (server) | WebSocket server for live team board |
| Service | Purpose |
|---|---|
| Resend | Transactional email delivery |
| Stripe | Subscription billing for paid plans |
| GitHub OAuth App | Authentication + GitHub API access for PR Queue |
| Slack Webhooks | Posting digests to team Slack channels (paid feature) |
| Tool | Purpose |
|---|---|
| Vercel | Frontend (Vite build) deployment with preview environments |
| Railway | Backend (Express) + Redis deployment |
| Neon | Serverless PostgreSQL. (branching, autoscaling) |
| GitHub Actions | CI pipeline. (lint, type-check on every push) |
| ESLint + Prettier | Code quality & consistent formatting |
| Husky + lint-staged | Pre-commit hooks. (lint before every commit) |
┌─────────────────────────────────────────────────────────┐
│ CLIENT │
│ React + Vite (Vercel) — SPA │
│ TanStack Query · Socket.io client · shadcn │
│ React Router v6 routing │
└────────────────────────┬────────────────────────────────┘
│ HTTP (REST) / WebSocket
┌────────────────────────▼────────────────────────────────┐
│ BACKEND │
│ Express API (Railway) │
│ Passport.js + JWT · Zod · Prisma │
└──────┬──────────────────┬──────────────────┬────────────┘
│ │ │
┌──────▼──────┐ ┌───────▼──────┐ ┌──────▼──────────┐
│ PostgreSQL │ │ Redis │ │ BullMQ │
│ (Neon) │ │ (Railway) │ │ Workers │
│ │ │ │ │ │
│ Users │ │ GitHub API │ │ Digest scheduler │
│ Teams │ │ cache │ │ PR poller │
│ Standups │ │ Rate limiter │ │ Reminder emails │
│ Digests │ │ Token store │ │ Slack poster │
│ Subscriptions│ │ │ │ │
└─────────────┘ └──────────────┘ └─────────────────-┘
│
┌─────────▼─────────┐
│ External Services │
│ Resend (email) │
│ Stripe (billing) │
│ GitHub API (PRs) │
│ Slack Webhooks │
└───────────────────┘
User clicks "Sign in with GitHub"
→ GET /api/auth/github → Passport redirects to GitHub OAuth
→ GitHub redirects back → GET /api/auth/github/callback
→ Passport exchanges code for access token
→ Fetch GitHub user profile (id, email, name, avatar)
→ Upsert User in PostgreSQL
→ Sign JWT with userId (secret from JWT_SECRET env var)
→ Return JWT in httpOnly cookie
→ Frontend stores nothing — cookie sent automatically on every request
→ Auth middleware on Express reads cookie, verifies JWT, attaches user to req
User fills form → POST /api/standups
→ Auth middleware verifies JWT
→ Zod validates body
→ Prisma writes to DB
→ GitHub URLs fetched (cached in Redis)
→ Socket.io emits "standup:submitted" to team room
→ All connected clients update live board instantly
→ BullMQ digest job checks if all members submitted (optional early send)
BullMQ cron job fires at team's digest time (UTC)
→ Fetch all standups for team for today
→ Compile digest — group by member, surface blockers first
→ Build plain HTML email string
→ Send via Resend to all team members
→ If Slack enabled → POST to team's Slack webhook URL
→ Write Digest record to DB with sentAt timestamp
model User {
id String @id @default(cuid())
email String @unique
name String?
avatar String?
githubId String? @unique
githubToken String? // encrypted — used for GitHub API calls
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships TeamMember[]
standups Standup[]
}
model Team {
id String @id @default(cuid())
name String
slug String @unique // used in URLs: /team/[slug]
ownerId String
digestTime String // "09:00" — stored in 24h format
timezone String // "Asia/Kolkata"
slackWebhook String? // paid feature
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members TeamMember[]
standups Standup[]
digests Digest[]
invites Invite[]
repos TeamRepo[] // connected GitHub repos for PR Queue
subscription Subscription?
}
model TeamMember {
id String @id @default(cuid())
teamId String
userId String
role Role @default(MEMBER) // OWNER | MEMBER
joinedAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([teamId, userId])
}
model Standup {
id String @id @default(cuid())
userId String
teamId String
done String // what I did
doing String // what I'm doing today
blocked String? // what's blocking me (optional)
submittedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
team Team @relation(fields: [teamId], references: [id])
links StandupLink[]
@@unique([userId, teamId, submittedAt]) // one standup per user per team per day
}
model StandupLink {
id String @id @default(cuid())
standupId String
url String
title String // fetched from GitHub API, cached in Redis
type LinkType // PR | ISSUE | OTHER
standup Standup @relation(fields: [standupId], references: [id])
}
model Digest {
id String @id @default(cuid())
teamId String
date String // "2024-01-15" — the date this digest covers
sentAt DateTime @default(now())
emailContent String // rendered HTML stored for history page
team Team @relation(fields: [teamId], references: [id])
}
model Invite {
id String @id @default(cuid())
teamId String
email String
token String @unique @default(cuid())
expiresAt DateTime
accepted Boolean @default(false)
createdAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id])
}
model TeamRepo {
id String @id @default(cuid())
teamId String
repoOwner String // "vercel"
repoName String // "next.js"
addedAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id])
@@unique([teamId, repoOwner, repoName])
}
model Subscription {
id String @id @default(cuid())
teamId String @unique
stripeCustomerId String
stripePriceId String
stripeSubId String
status SubStatus // ACTIVE | PAST_DUE | CANCELED | TRIALING
currentPeriodEnd DateTime
createdAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id])
}
enum Role { OWNER MEMBER }
enum LinkType { PR ISSUE OTHER }
enum SubStatus { ACTIVE PAST_DUE CANCELED TRIALING }Note: The
planfield was removed fromUser— plan is tracked at theTeamlevel viaSubscription, not per user. A user can belong to a free team and a paid team simultaneously.
GET /api/auth/github Initiate GitHub OAuth flow (Passport)
GET /api/auth/github/callback GitHub OAuth callback — issue JWT cookie
GET /api/auth/google Initiate Google OAuth flow (Passport)
GET /api/auth/google/callback Google OAuth callback — issue JWT cookie
POST /api/auth/logout Clear JWT cookie
GET /api/auth/me Return current authenticated user
POST /api/teams Create a new team
GET /api/teams/:slug Get team details + members
PATCH /api/teams/:slug Update digest time, timezone, Slack webhook
DELETE /api/teams/:slug Delete team (owner only)
GET /api/teams/:slug/members List team members with roles
DELETE /api/teams/:slug/members/:userId Remove a member
POST /api/teams/:slug/invites Send an invite email to an address
GET /api/invites/:token Validate an invite token
POST /api/invites/:token/accept Accept invite and join team
POST /api/standups Submit today's standup
GET /api/teams/:slug/standups Get standups (date range filter)
GET /api/teams/:slug/standups/today Get today's submissions + who hasn't submitted
GET /api/teams/:slug/digests Get digest history for a team
GET /api/teams/:slug/digests/:date Get a specific digest by date
POST /api/teams/:slug/digests/send Manually trigger digest (owner only)
POST /api/teams/:slug/repos Add a GitHub repo to watch
DELETE /api/teams/:slug/repos/:id Remove a repo
GET /api/teams/:slug/prs Get scored PR queue for the team
POST /api/teams/:slug/prs/refresh Force a PR data refresh from GitHub
GET /api/github/link-preview Fetch title for a GitHub URL (Redis cached)
POST /api/billing/checkout Create Stripe checkout session
POST /api/billing/portal Create Stripe customer portal session
POST /api/billing/webhook Stripe webhook handler (subscription events)
This is a separate static deployment on Vercel. No auth required. Purpose is to explain the product, show pricing, and funnel visitors to the app.
| Route | Description |
|---|---|
/ |
Hero section — product pitch, headline, CTA button → app.mystandup.in/login |
/features |
Detailed feature breakdown — standups, PR queue, digest engine |
/pricing |
Pricing table — free vs paid plan comparison |
/changelog |
Public changelog — what's new in each release |
/blog |
Optional — articles about async work and dev team culture |
The full React + Vite SPA. All routes require authentication except /login and /invite/:token.
| Route | Description |
|---|---|
/login |
Auth page — GitHub OAuth, Google OAuth |
/dashboard |
User home — list of all teams they belong to |
/team/new |
Create a new team — name, slug, digest time, timezone |
/team/:slug |
Team home — live board (who submitted today) + today's standups |
/team/:slug/standup |
Standup form — done, doing, blocked + GitHub link attachment |
/team/:slug/reviews |
PR Review Queue — scored open PRs across connected repos |
/team/:slug/history |
Past standups — filterable by member and date range |
/team/:slug/digests |
Digest history — all past digests with full content |
/team/:slug/settings |
Team settings — digest time, Slack webhook, repos, members, billing |
/invite/:token |
Invite acceptance — join a team by clicking the email link |
/settings |
User settings — profile, notification preferences |
Why the split? The marketing site (
mystandup.in) is a lightweight static page that loads instantly and is optimised for SEO and conversions. The app (app.mystandup.in) is a full React SPA that requires auth. Keeping them on separate subdomains means the marketing site never gets slowed down by app bundle size, and the app never bleeds into public search results.
Note: React Router v6 uses
:paramsyntax, not[param]like Next.js. File-based routing does not exist — all routes are defined manually in a central router file.
GitHub's native PR interface gives no priority signal. A developer looking at 10 open PRs has no idea:
- Which one is blocking a teammate
- Which has been waiting the longest
- Which has CI passing and just needs one more approval
- Which they were explicitly requested to review
- Team admin connects GitHub repos in team settings (up to 3 on free, unlimited on paid)
- A BullMQ worker polls connected repos every 15 minutes via GitHub REST API
- Each open PR gets a priority score calculated as:
priority_score =
(hours_since_opened × 0.35) // age — older PRs float to top
+ (approvals_still_needed × 0.25) // closer to merge = more urgent
+ (ci_passing ? 0 : 0.20) // failing CI reduces priority
+ (you_are_requested_reviewer ? 0.15 : 0) // explicit requests prioritized
+ (author_is_teammate ? 0.05 : 0) // team PRs over external
// score normalized to 0–100
- Each team member sees their personal queue — PRs where they are a requested reviewer or have not yet reviewed, sorted by score descending
- Results cached in Redis with a 15-minute TTL
- Force refresh available for team owners
BullMQ PR poller (every 15 min)
→ Fetch open PRs from GitHub API for each connected repo
→ For each PR: fetch reviews, CI status, requested reviewers
→ Calculate priority score for each PR
→ Store in Redis: key = "prs:{teamId}", TTL = 15 min
→ Emit Socket.io event "prs:updated" to team room
→ Clients refetch from Redis cache via GET /api/teams/:slug/prs
GitHub API allows 5,000 requests/hour per authenticated user. Strategy:
- All GitHub API calls go through a single queue per GitHub token
- Responses cached in Redis with appropriate TTL (PR list: 15 min, PR details: 5 min)
- If rate limit is hit, BullMQ job is delayed until reset time from
X-RateLimit-Resetheader - Teams share a pool of authenticated tokens if multiple members have connected GitHub
Each PR card in the queue shows:
- PR title + repo name + PR number
- Author avatar and name
- Priority score badge (red = urgent · amber = medium · green = low)
- Hours open
- CI status indicator (passing / failing / pending)
- Approval count (e.g. "1/2 approvals")
- Your review status (requested / reviewed / not requested)
- Direct link to open the PR on GitHub
Team created with digestTime: "09:00", timezone: "Asia/Kolkata"
→ Convert to UTC: "09:00 IST" = "03:30 UTC"
→ BullMQ repeatable job created: cron "30 3 * * *"
→ Job fires every day at 03:30 UTC
→ Worker fetches all standups for that team submitted since yesterday's digest
→ Compile digest object:
{
date: "2024-01-15",
members_submitted: [...],
members_missed: [...],
blockers: [...], // all "blocked" fields that are non-empty
standups: [...]
}
→ Build plain HTML email string from digest object
→ Send to all team members via Resend
→ If Slack enabled → POST compiled plain text to Slack webhook URL
→ Write Digest record to PostgreSQL
- Store digest time as "HH:MM" string in team settings
- Store timezone as IANA string ("Asia/Kolkata", "America/New_York")
- On team create/update → recalculate UTC offset → update BullMQ cron expression
- Handle DST: recalculate offset on every job run, not just at creation
1 hour before digest time:
→ BullMQ reminder job fires
→ Fetch today's submissions for the team
→ Find members who haven't submitted
→ Send plain HTML reminder email to each non-submitter:
"Your team's standup digest is in 1 hour. Submit yours here →"
| Event | Trigger | Who sees it |
|---|---|---|
standup:submitted |
Member submits standup | All team members on the team board |
prs:updated |
BullMQ PR poller completes | All members on the PR review page |
member:joined |
Invite accepted | All members on the team board |
Each team gets its own Socket.io room: "team:{teamId}"
On client connect:
→ Authenticate via JWT from cookie
→ Join room for each team the user belongs to
On standup submit:
→ Server emits to room "team:{teamId}"
→ All connected clients receive "standup:submitted" event
→ TanStack Query invalidates "standups/today" query
→ Live board re-renders with updated submission status
Polling every 5 seconds means 12 requests/minute per user. With 10 users on a team that's 120 requests/minute just for the live board. WebSockets hold one persistent connection per client and push updates only when something actually changes — far more efficient.
- Resend — transactional email delivery. 100 emails/day free, then $20/mo for 50k.
- Plain HTML strings — email templates built as plain HTML template literals in TypeScript. Simple to write, no extra dependencies, handles cross-client compatibility when kept minimal.
Upgrade to React Email post-MVP if templates grow complex enough to justify it.
| Trigger | Notes | |
|---|---|---|
| Team invite | Admin invites someone | Team name, inviter name, CTA button |
| Daily digest | BullMQ cron job | Blockers first, standups per member, missed list |
| Reminder | 1 hour before digest (paid) | Simple nudge with submit link |
| Welcome | User signs up for first time | Product intro + "create your first team" CTA |
| Payment failed | Stripe webhook | Prompt to update billing details |
Subject: "Daily Standup — {Team Name} — {Date}"
─────────────────────────────────
🚨 BLOCKERS (if any)
• [Member name]: {blocked text}
─────────────────────────────────
✅ {Member Name}
Done: {done text}
Doing: {doing text}
✅ {Member Name}
Done: {done text}
Doing: {doing text}
─────────────────────────────────
❌ Didn't submit: {name}, {name}
─────────────────────────────────
View full history → mystandup.in/team/{slug}/history
1. User clicks "Upgrade to Paid" in team settings
2. POST /api/billing/checkout
→ Create Stripe customer if not exists
→ Create Stripe checkout session (subscription mode)
→ Redirect user to Stripe hosted checkout page
3. User completes payment on Stripe
4. Stripe sends webhook → POST /api/billing/webhook
→ Event: "checkout.session.completed"
→ Create Subscription record in DB
→ User redirected to /team/:slug/settings with success message
| Event | Action |
|---|---|
checkout.session.completed |
Activate subscription, set status to ACTIVE |
invoice.payment_succeeded |
Extend currentPeriodEnd, keep plan active |
invoice.payment_failed |
Send payment failed email, set status to PAST_DUE |
customer.subscription.deleted |
Set status to CANCELED, revert to free limits |
// middleware/planGate.ts
export const requirePaid = async (teamId: string) => {
const sub = await prisma.subscription.findUnique({ where: { teamId } });
const isPaid = sub?.status === "ACTIVE" || sub?.status === "TRIALING";
if (!isPaid) throw new PlanError("This feature requires a paid plan");
};
// Usage — POST /api/teams/:slug/repos (4th repo onward)
const repoCount = await prisma.teamRepo.count({ where: { teamId } });
if (repoCount >= 3) await requirePaid(team.id);standup-dev/
├── apps/
│ ├── marketing/ # Static marketing site (mystandup.in)
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── src/
│ │ │ ├── main.tsx
│ │ │ ├── App.tsx # React Router — marketing routes only
│ │ │ ├── pages/
│ │ │ │ ├── Home.tsx # / — hero, features overview, CTA
│ │ │ │ ├── Features.tsx # /features
│ │ │ │ ├── Pricing.tsx # /pricing
│ │ │ │ └── Changelog.tsx # /changelog
│ │ │ ├── components/
│ │ │ │ ├── Hero.tsx
│ │ │ │ ├── FeatureGrid.tsx
│ │ │ │ ├── PricingTable.tsx
│ │ │ │ ├── Navbar.tsx
│ │ │ │ └── Footer.tsx
│ │ │ └── lib/
│ │ │ └── utils.ts
│ │ └── package.json
│ │
│ ├── web/ # React + Vite app (app.mystandup.in)
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── src/
│ │ │ ├── main.tsx # React app entry — mounts <App />
│ │ │ ├── App.tsx # Router definition (React Router v6)
│ │ │ ├── pages/
│ │ │ │ ├── Login.tsx # /login
│ │ │ │ ├── Dashboard.tsx # /dashboard
│ │ │ │ ├── TeamNew.tsx # /team/new
│ │ │ │ ├── TeamBoard.tsx # /team/:slug
│ │ │ │ ├── StandupForm.tsx # /team/:slug/standup
│ │ │ │ ├── PRReviewQueue.tsx # /team/:slug/reviews
│ │ │ │ ├── StandupHistory.tsx # /team/:slug/history
│ │ │ │ ├── DigestHistory.tsx # /team/:slug/digests
│ │ │ │ ├── TeamSettings.tsx # /team/:slug/settings
│ │ │ │ ├── InviteAccept.tsx # /invite/:token
│ │ │ │ └── UserSettings.tsx # /settings
│ │ │ ├── components/
│ │ │ │ ├── ui/ # shadcn/ui components
│ │ │ │ ├── team/
│ │ │ │ ├── standup/
│ │ │ │ ├── reviews/
│ │ │ │ └── shared/
│ │ │ ├── hooks/
│ │ │ │ ├── useAuth.ts
│ │ │ │ ├── useSocket.ts
│ │ │ │ └── useTeam.ts
│ │ │ ├── lib/
│ │ │ │ ├── api.ts # Axios instance + TanStack Query fetchers
│ │ │ │ ├── socket.ts # Socket.io client setup
│ │ │ │ └── utils.ts
│ │ │ └── types/
│ │ └── package.json
│ │
│ └── api/ # Express backend (Railway)
│ ├── src/
│ │ ├── routes/
│ │ │ ├── auth.ts # Passport OAuth + JWT cookie
│ │ │ ├── teams.ts
│ │ │ ├── standups.ts
│ │ │ ├── invites.ts
│ │ │ ├── digests.ts
│ │ │ ├── prs.ts
│ │ │ ├── github.ts
│ │ │ └── billing.ts
│ │ ├── workers/
│ │ │ ├── digest.worker.ts # BullMQ digest job
│ │ │ ├── pr-poller.worker.ts # BullMQ PR polling job
│ │ │ └── reminder.worker.ts # BullMQ reminder email job
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT cookie verification
│ │ │ ├── planGate.ts # Free/paid feature gating
│ │ │ └── rateLimit.ts # Redis-based rate limiting
│ │ ├── lib/
│ │ │ ├── prisma.ts
│ │ │ ├── redis.ts
│ │ │ ├── queue.ts # BullMQ setup
│ │ │ ├── socket.ts # Socket.io server setup
│ │ │ ├── github.ts # GitHub API client
│ │ │ ├── resend.ts # Email client
│ │ │ ├── stripe.ts # Stripe client
│ │ │ └── passport.ts # Passport strategy config
│ │ ├── scoring/
│ │ │ └── pr-score.ts # PR priority scoring algorithm
│ │ └── index.ts # Express app entry
│ └── package.json
│
├── packages/
│ ├── db/ # Shared Prisma schema
│ │ ├── schema.prisma
│ │ └── package.json
│ └── types/ # Shared TypeScript types
│ ├── index.ts
│ └── package.json
│
├── .github/
│ └── workflows/
│ └── ci.yml # Lint + type-check on every push
├── turbo.json # Turborepo config
├── package.json # Root workspace
└── .env.example
Two Vercel deployments from one repo: In Vercel, create two projects pointing at the same GitHub repo — one for
apps/marketing(root directory:apps/marketing) pointed atmystandup.in, and one forapps/web(root directory:apps/web) pointed atapp.mystandup.in. Turborepo handles building only what changed.
# ── Database ──────────────────────────────────────────────
DATABASE_URL="postgresql://..." # Neon connection string
# ── Redis ─────────────────────────────────────────────────
REDIS_URL="redis://..." # Railway Redis URL
# ── Auth ──────────────────────────────────────────────────
JWT_SECRET="..." # Random 64-char string — sign JWT tokens
JWT_EXPIRES_IN="7d" # JWT expiry duration
GITHUB_CLIENT_ID="..." # GitHub OAuth App client ID
GITHUB_CLIENT_SECRET="..." # GitHub OAuth App client secret
GOOGLE_CLIENT_ID="..." # Google OAuth App client ID
GOOGLE_CLIENT_SECRET="..." # Google OAuth App client secret
# ── Email ─────────────────────────────────────────────────
RESEND_API_KEY="re_..."
EMAIL_FROM="noreply@mystandup.in"
# ── Stripe ────────────────────────────────────────────────
STRIPE_SECRET_KEY="sk_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_PRICE_ID="price_..." # Paid plan price ID from Stripe dashboard
# ── GitHub API ────────────────────────────────────────────
GITHUB_APP_TOKEN="ghp_..." # Server-side token for PR polling
# ── App ───────────────────────────────────────────────────
CLIENT_URL="http://localhost:5173" # Vite default port — update for production
API_URL="http://localhost:4000"
NODE_ENV="development"
COOKIE_DOMAIN="localhost" # Set to ".mystandup.in" in productionGoal: Project scaffolded, auth working, user can create a team.
- Initialise Turborepo monorepo —
apps/marketing(Vite),apps/web(Vite),apps/api(Express),packages/db,packages/types - Configure ESLint, Prettier, Husky, lint-staged across all packages
- Set up Prisma schema — User, Team, TeamMember, Invite — run first migration on Neon
- Set up Passport.js — GitHub OAuth strategy + Google OAuth strategy
- JWT cookie auth middleware — sign token on OAuth callback, verify on protected routes
-
GET /api/auth/me— return current user from JWT - Build
/loginpage — GitHub + Google OAuth buttons - Build
/dashboard— list user's teams (empty state to start) - Build
/team/new— create team form with slug uniqueness validation - Invite system — send invite email via Resend,
/invite/:tokenacceptance flow - GitHub Actions CI — lint + type-check on every pull request
Milestone: User can sign in with GitHub or Google, create a team, and invite a teammate.
Goal: The standup form works end-to-end. Live board updates in real-time.
- Build standup form — done, doing, blocked fields + React Hook Form + Zod validation
- GitHub URL input — paste a URL →
GET /api/github/link-preview→ cache title in Redis -
POST /api/standups— validate, write to DB, emit Socket.io event - Set up Socket.io server on Express — room per team
- Set up Socket.io client in React —
useSocketcustom hook - Build
/team/:slugteam board — member grid with submitted / pending status - Live board —
standup:submittedevent updates board without page refresh - Today's standups panel — expandable cards per member with done/doing/blocked
- Blocker section — surface all non-empty blocked fields in a highlighted panel
- Build
/team/:slug/history— past standups with date picker + member filter
Milestone: A team can complete their daily standup. The board updates live as members submit.
Goal: Teams receive an automated daily email digest at their configured time.
- Set up BullMQ in
apps/apiwith Redis connection - Build digest worker — compile today's standups into digest object
- Build plain HTML digest email template — blockers first, standups per member, missed list
- Set up Resend client —
POST /api/teams/:slug/digests/sendtriggers manually first - Timezone-aware cron scheduling — convert "HH:MM + IANA timezone" to UTC cron expression
- On team create/update → create/update BullMQ repeatable digest job
- Build reminder worker stub — fires 1 hour before digest time (full logic in week 5)
- Build
/team/:slug/digests— digest history page showing rendered HTML content - Test full digest flow end-to-end — create team, submit standups, receive real email
Milestone: Create a team, configure digest time, submit standups, receive the email at the right time.
Goal: Teams can connect GitHub repos and see a prioritized PR queue updating every 15 minutes.
- Add
TeamRepotable to Prisma schema — run migration - Repo management UI in team settings — add/remove repos with plan gating (3 max on free)
- Build PR polling BullMQ worker — polls every 15 min per team
- GitHub API integration — fetch open PRs, reviews, CI check runs, requested reviewers
- Implement priority scoring algorithm in
apps/api/src/scoring/pr-score.ts - Cache scored PRs in Redis — key
prs:{teamId}, TTL 15 min -
GET /api/teams/:slug/prs— return cached sorted PR list -
POST /api/teams/:slug/prs/refresh— force refresh for team owners - Emit
prs:updatedSocket.io event when poller completes - Build
/team/:slug/reviews— PR cards with score badge, CI status, approval count - GitHub API rate limit handling — delay job on 429 until
X-RateLimit-Reset
Milestone: Connect a real GitHub repo — see open PRs scored and sorted, refreshing automatically.
Goal: Stripe billing works. Slack works. App is live on mystandup.in.
- Stripe setup — create product + price in Stripe dashboard
-
POST /api/billing/checkout— create Stripe checkout session -
POST /api/billing/portal— Stripe customer portal for managing subscription -
POST /api/billing/webhook— handle checkout, payment success/fail, cancellation - Plan gating middleware — enforce member limits, repo limits on all relevant routes
- Slack webhook integration — POST digest to team's Slack channel (paid)
- Custom standup questions — add/remove extra fields in team settings (paid)
- Reminder emails — complete reminder worker with Resend send (paid)
- Build marketing site (
apps/marketing) — hero, features, pricing table, CTA →app.mystandup.in/login - Deploy marketing site to Vercel — root directory
apps/marketing, pointmystandup.indomain - Deploy app frontend to Vercel — root directory
apps/web, pointapp.mystandup.indomain - Deploy API to Railway — set env vars, connect Neon DB + Redis
- Run production DB migration
- Manual smoke test on production — full flow end-to-end
- Set up Sentry free tier for error monitoring
Milestone: Marketing site live at mystandup.in. App live at app.mystandup.in. Stripe billing works, digests are sending reliably.
Use only if needed. Prioritise in this order:
- Bug fixes from real usage or testing with a second person
- Empty states, loading skeletons, error boundaries throughout the UI
- Mobile responsiveness — team board and standup form must work on phone
- Onboarding flow — first-time user guidance after sign in
- README + project documentation for GitHub repo
| Concept | Where it shows up |
|---|---|
| Weighted scoring / ranking | PR priority score — multi-factor weighted algorithm normalized to 0–100 |
| Graph | Team membership model — member nodes, role edges, permission traversal |
| Hash map | Redis key-value store — GitHub API response cache, PR score cache |
| Queue (FIFO) | BullMQ job queue — digest jobs, PR polling jobs, reminder jobs |
| Cron scheduling | Timezone-aware repeatable jobs — local time to UTC cron conversion |
| Token bucket | GitHub API rate limit handling — throttle requests per authenticated token |
| Cryptographic hashing | Invite token generation with crypto.randomBytes — collision resistant |
| Pub/Sub | Socket.io rooms — event broadcasting to all clients in a team room |
| TTL-based caching | Redis cache-aside pattern — PR data, GitHub link titles with expiry |
| String parsing | GitHub URL parsing — extract owner, repo name, PR/issue number |
| Product | Pricing | Weakness vs Standup.dev |
|---|---|---|
| Geekbot | $2.50/user/month | Per-user pricing expensive for growing teams. Slack-only, no web UI. |
| Standuply | $5/user/month | Over-engineered. No PR review queue. |
| Range.co | $6/user/month | No PR queue. Heavy, bloated tool for small teams. |
| Dailybot | $3/user/month | Slack-dependent. No standalone web interface. |
| Status Hero | $3/user/month | Dated UI. No GitHub PR integration. |
Standup.dev's edge:
- Flat team pricing ($9/team) — cheaper for teams of 4+ than per-user competitors
- PR Review Queue built in — no other tool in this category has it
- Works without Slack — email-first with Slack as a paid add-on
- Clean, modern UI built with shadcn + Tailwind
-
🎨 Any improvements to the design & UI are welcome.
-
🔨 Try to break the app by testing it to find any bugs. If you find any, check if there is an issue already open for it. If there is none, then report it.
-
💡 All code must be written in TypeScript — no
anytypes. All API inputs must be validated with Zod. -
📱 For UI changes, test on both desktop and mobile viewport sizes before submitting.
-
Fork the standup-dev repo by clicking on the fork button on the top of the page. This will create a copy of this repository in your account.
-
Clone the forked repository
git clone "https://github.com/<your-github-username>/standup-dev"Then set up your local environment:
-
Download and install Node.js v18 or higher
-
Download and install Git
-
Download and install pnpm :
npm install -g pnpm -
Install Docker and start it : used to run PostgreSQL and Redis locally
-
Navigate into the project and install dependencies:
cd standup-dev pnpm install -
Copy the example env file and fill in your values:
cp .env.example .env
-
Start PostgreSQL and Redis via Docker:
docker compose up -d
-
Run the database migration:
pnpm db:migrate
-
Start all apps in development mode:
pnpm dev
After running
pnpm devyou should have:- Marketing site at
http://localhost:5174 - App at
http://localhost:5173 - API at
http://localhost:4000
Setting up GitHub OAuth locally:
- Go to github.com/settings/developers → New OAuth App
- Set Homepage URL to
http://localhost:5173 - Set Authorization callback URL to
http://localhost:4000/api/auth/github/callback - Copy Client ID and Client Secret into your
.env
Setting up Google OAuth locally:
- Go to console.cloud.google.com → create a project → APIs & Services → Credentials
- Create an OAuth 2.0 Client ID (Web application)
- Add
http://localhost:4000/api/auth/google/callbackas an authorised redirect URI - Copy Client ID and Client Secret into your
.env
-
-
Make necessary changes & commit those changes.
Remember, never push anything directly to the
mainbranch.Always switch your branch to
developfirst:git checkout develop
Verify your current branch:
git branch
It should show
* developAdd your changes:
git add files-you-edited
If there are multiple files:
git add .Create a commit message following the Conventional Commits standard:
git commit -m "<type>: <short description>"Prefix Use for feat:A new feature fix:A bug fix docs:Documentation changes only style:Formatting, missing semicolons — no logic change refactor:Code restructure — no feature or bug change test:Adding or updating tests chore:Build process, dependency updates, tooling Run lint and type checks before pushing — the CI pipeline will reject failures:
pnpm lint pnpm typecheck
-
Push changes to GitHub.
git push origin develop
-
Create a Pull Request. 👋
Go to your repository on GitHub, you'll see a Compare & pull request button.
Click it & write a summary of what changes you made (attach screenshots for any UI changes).
I will review your code & merge it if it passes all checks. ❤️
Before opening a PR, always check:
-
pnpm lintpasses with no errors -
pnpm typecheckpasses with no errors - The feature works correctly on both
http://localhost:5173(app) andhttp://localhost:5174(marketing) if applicable - You've commented on the related issue so others know it's being worked on
Getting help:
- Open a GitHub Discussion for general questions
- Comment directly on the issue you're working on
- Reach out on Twitter
-