Skip to content

mrinnnmoy/Standup-dev

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Standup.dev

Freemium team coordination hub. Async daily standups with automated email digests, live team boards & a prioritized GitHub PR review queue.


Table of Contents.

  1. Overview
  2. Problem & Solution
  3. Target Users
  4. Business Model
  5. Feature Scope
  6. Tech Stack
  7. System Architecture
  8. Database Schema
  9. API Routes
  10. Pages & Routes
  11. PR Review Queue Feature
  12. Digest Engine
  13. Real-time (WebSockets)
  14. Email System
  15. Payments (Stripe)
  16. Folder Structure
  17. Environment Variables
  18. 5-Week Build Plan
  19. DSA & CS Concepts Exercised
  20. Competitors
  21. How to Contribute.

1. Overview

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)

2. Problem & Solution

The Problem

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.

The Solution

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.


3. Target Users

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

4. Business Model

Free Plan

  • Up to 5 members per team
  • Unlimited standups and history
  • Daily email digest
  • PR Review Queue (up to 3 repos)
  • Live team board

Paid Plan — $9/month per team

  • 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

Pricing rationale

  • 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

5. Feature Scope

✅ Free tier. (Build in MVP)

  • 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.

💜 Paid tier. (Build in MVP)

  • 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)

🔒 Post-MVP.

  • 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

6. Tech Stack

Frontend

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

Backend

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

Data & Infrastructure

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

External Services

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)

DevOps

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)

7. System Architecture

┌─────────────────────────────────────────────────────────┐
│                        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    │
                                    └───────────────────┘

Auth flow — GitHub OAuth + JWT

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

Request flow — standup submission

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)

Request flow — digest delivery

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

8. Database Schema

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 plan field was removed from User — plan is tracked at the Team level via Subscription, not per user. A user can belong to a free team and a paid team simultaneously.


9. API Routes

Auth

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

Teams

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

Invites

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

Standups

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

Digests

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)

PR Review Queue

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

GitHub

GET    /api/github/link-preview             Fetch title for a GitHub URL (Redis cached)

Billing

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)

10. Pages & Routes

Marketing site — mystandup.in

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

App — app.mystandup.in

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 :param syntax, not [param] like Next.js. File-based routing does not exist — all routes are defined manually in a central router file.


11. PR Review Queue Feature

The problem it solves

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

How it works

  1. Team admin connects GitHub repos in team settings (up to 3 on free, unlimited on paid)
  2. A BullMQ worker polls connected repos every 15 minutes via GitHub REST API
  3. 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
  1. Each team member sees their personal queue — PRs where they are a requested reviewer or have not yet reviewed, sorted by score descending
  2. Results cached in Redis with a 15-minute TTL
  3. Force refresh available for team owners

Data flow

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

Rate limit handling

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-Reset header
  • Teams share a pool of authenticated tokens if multiple members have connected GitHub

PR card UI

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

12. Digest Engine

How scheduled digests work

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

Timezone handling

  • 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

Missed standup reminder (paid)

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 →"

13. Real-time (WebSockets)

What updates in real-time

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

Socket.io room structure

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

Why not just polling?

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.


14. Email System

Tools

  • 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.

Email types

Email 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

Digest email structure

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

15. Payments (Stripe)

Subscription flow

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

Webhook events to handle

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

Feature gating

// 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);

16. Folder Structure

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 at mystandup.in, and one for apps/web (root directory: apps/web) pointed at app.mystandup.in. Turborepo handles building only what changed.


17. Environment Variables

# ── 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 production

18. 6-Week Build Plan

Week 1 — Foundation

Goal: 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 /login page — 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/:token acceptance 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.


Week 2 — Core standup loop

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 — useSocket custom hook
  • Build /team/:slug team board — member grid with submitted / pending status
  • Live board — standup:submitted event 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.


Week 3 — Digest engine

Goal: Teams receive an automated daily email digest at their configured time.

  • Set up BullMQ in apps/api with 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/send triggers 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.


Week 4 — PR Review Queue

Goal: Teams can connect GitHub repos and see a prioritized PR queue updating every 15 minutes.

  • Add TeamRepo table 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:updated Socket.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.


Week 5 — Paid features + deploy

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, point mystandup.in domain
  • Deploy app frontend to Vercel — root directory apps/web, point app.mystandup.in domain
  • 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.


Week 6 — Polish (buffer week)

Use only if needed. Prioritise in this order:

  1. Bug fixes from real usage or testing with a second person
  2. Empty states, loading skeletons, error boundaries throughout the UI
  3. Mobile responsiveness — team board and standup form must work on phone
  4. Onboarding flow — first-time user guidance after sign in
  5. README + project documentation for GitHub repo

19. DSA & CS Concepts Exercised

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

20. Competitors

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

21. How to Contribute.

  • 🎨 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 any types. All API inputs must be validated with Zod.

  • 📱 For UI changes, test on both desktop and mobile viewport sizes before submitting.

🔃 Steps to be followed in order to make valid contributions to this repo.

  1. 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.

  2. 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 dev you should have:

    • Marketing site at http://localhost:5174
    • App at http://localhost:5173
    • API at http://localhost:4000

    Setting up GitHub OAuth locally:

    1. Go to github.com/settings/developersNew OAuth App
    2. Set Homepage URL to http://localhost:5173
    3. Set Authorization callback URL to http://localhost:4000/api/auth/github/callback
    4. Copy Client ID and Client Secret into your .env

    Setting up Google OAuth locally:

    1. Go to console.cloud.google.com → create a project → APIs & ServicesCredentials
    2. Create an OAuth 2.0 Client ID (Web application)
    3. Add http://localhost:4000/api/auth/google/callback as an authorised redirect URI
    4. Copy Client ID and Client Secret into your .env
  3. Make necessary changes & commit those changes.

    Remember, never push anything directly to the main branch.

    Always switch your branch to develop first:

    git checkout develop

    Verify your current branch:

    git branch

    It should show * develop

    Add 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
  4. Push changes to GitHub.

    git push origin develop
  5. 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 lint passes with no errors
    • pnpm typecheck passes with no errors
    • The feature works correctly on both http://localhost:5173 (app) and http://localhost:5174 (marketing) if applicable
    • You've commented on the related issue so others know it's being worked on

    Getting help:


About

Async standups for dev teams.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors