Skip to content

Latest commit

Β 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚑ Pulsar Dev

Your projects, one dashboard.

A real-time DevOps dashboard for small development teams. Monitor GitHub activity, deployments, and infrastructure metrics in one unified interface with live WebSocket updates.

Built for teams of 3-10 developers who are tired of switching between GitHub, CI/CD dashboards, and monitoring tools.


πŸ“‹ Table of Contents


🎯 Overview

The Problem

Development teams waste time context-switching between multiple tools:

  • GitHub for PRs and code reviews
  • Vercel/Railway for deployment status
  • DataDog/monitoring tools for infrastructure
  • Slack for notifications

The Solution

Pulsar Dev aggregates all critical project information into a single, real-time dashboard. Know what's happening across all your projects without switching tabs.

Key Value Propositions

  1. Real-time updates - See changes as they happen via WebSocket
  2. Single pane of glass - All project data in one place
  3. Extensible - Plugin architecture for adding new integrations
  4. Free & open source - Self-host or use our hosted version

πŸ›  Tech Stack

Backend

  • Language: Go 1.21+
  • HTTP Framework: Chi (lightweight, idiomatic router)
  • WebSocket: gorilla/websocket
  • Database: Neon PostgreSQL (serverless)
  • Cache/Pub-Sub: Redis (Upstash for production)
  • Database Driver: pgx/v5
  • Migrations: golang-migrate

Frontend

  • Framework: Next.js 14+ (App Router)
  • Language: TypeScript
  • State Management:
    • React Query/TanStack Query (server state)
    • Zustand (UI state)
  • UI Library: shadcn/ui + Tailwind CSS
  • WebSocket Client: Native WebSocket API

Infrastructure

  • Frontend Hosting: Vercel
  • Backend Hosting: Railway
  • Database: Neon (managed PostgreSQL)
  • Redis: Upstash (serverless Redis)
  • CI/CD: GitHub Actions
  • Containerization: Docker + Docker Compose (local dev)

External Integrations

  • GitHub API (primary integration)
  • Vercel API (future)
  • Railway API (future)
  • Custom webhook support (future)

πŸ— Architecture

High-Level System Design

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Browser                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚         Next.js Frontend (React)                     β”‚   β”‚
β”‚  β”‚  - Dashboard UI                                      β”‚   β”‚
β”‚  β”‚  - WebSocket Client                                  β”‚   β”‚
β”‚  β”‚  - React Query (caching)                             β”‚   β”‚
β”‚  β”‚  - Zustand (UI state)                                β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚ HTTP/REST                β”‚ WebSocket
                β”‚                          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Go Backend (Railway)                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  REST API    β”‚  β”‚  WebSocket   β”‚  β”‚    Auth      β”‚      β”‚
β”‚  β”‚  (Chi)       β”‚  β”‚  Hub         β”‚  β”‚  Middleware  β”‚      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚         β”‚                  β”‚                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚           Application Services                      β”‚     β”‚
β”‚  β”‚  - ProjectService                                   β”‚     β”‚
β”‚  β”‚  - IntegrationService                               β”‚     β”‚
β”‚  β”‚  - MetricsService                                   β”‚     β”‚
β”‚  β”‚  - WebSocketService                                 β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚         β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚         Background Workers (Go routines)            β”‚    β”‚
β”‚  β”‚  - GitHub Poller (polls every 30-60s)               β”‚    β”‚
β”‚  β”‚  - Deployment Poller (future)                       β”‚    β”‚
β”‚  β”‚  - Custom Integration Pollers                       β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Data Layer                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚  β”‚    Neon      β”‚  β”‚    Redis     β”‚                        β”‚
β”‚  β”‚ PostgreSQL   β”‚  β”‚  (Upstash)   β”‚                        β”‚
β”‚  β”‚              β”‚  β”‚              β”‚                        β”‚
β”‚  β”‚ - Projects   β”‚  β”‚ - Cache      β”‚                        β”‚
β”‚  β”‚ - Users      β”‚  β”‚ - Pub/Sub    β”‚                        β”‚
β”‚  β”‚ - Metrics    β”‚  β”‚ - Sessions   β”‚                        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                  β”‚
          β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚         β”‚ Redis Pub/Sub   β”‚
          β”‚         β”‚   Channel       β”‚
          β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                  β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚  External APIs    β”‚
          β”‚  - GitHub         β”‚
          β”‚  - Vercel         β”‚
          β”‚  - Railway        β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow: Real-Time Update

This is how a GitHub PR update flows through the system:

1. [GitHub Poller Worker]
   └─> Polls GitHub API every 30 seconds
   └─> GET /repos/:owner/:repo/pulls?state=open
   
2. [GitHub Poller Worker]
   └─> Detects new PR #42 opened
   └─> Compares with last known state in PostgreSQL
   
3. [GitHub Poller Worker]
   └─> Saves to Neon PostgreSQL
   └─> INSERT INTO pull_requests (...)
   
4. [GitHub Poller Worker]
   └─> Publishes event to Redis
   └─> PUBLISH "project:123:events" '{"type":"pr_opened","pr_id":42,...}'
   
5. [WebSocket Hub]
   └─> Subscribed to Redis channel
   └─> Receives event via Redis Pub/Sub
   
6. [WebSocket Hub]
   └─> Identifies relevant connected clients
   └─> Filters by project_id and user permissions
   
7. [WebSocket Hub]
   └─> Broadcasts to connected clients
   └─> ws.Send({"type":"pr_opened","data":{...}})
   
8. [Frontend - React]
   └─> WebSocket client receives message
   └─> React Query cache invalidated
   └─> UI re-renders with new PR
   └─> User sees update instantly (< 1 second from GitHub change)

Component Responsibilities

Backend Components

1. HTTP Server (Chi Router)

  • Handle REST API requests
  • Serve API endpoints
  • Authentication middleware
  • CORS handling
  • Request validation

2. WebSocket Hub

  • Manage WebSocket connections
  • Route messages to correct clients
  • Handle connection lifecycle
  • Implement heartbeat (ping/pong)
  • User/project-based message filtering

3. Application Services

  • Business logic layer
  • Database operations (CRUD)
  • Integration orchestration
  • Data transformation
  • Permission checks

4. Background Workers

  • Poll external APIs (GitHub, etc.)
  • Process and transform data
  • Save to database
  • Publish events to Redis
  • Handle rate limits and retries

5. Redis Subscriber

  • Listen to Redis pub/sub channels
  • Forward events to WebSocket Hub
  • Decouple workers from WebSocket server

Frontend Components

1. Next.js App Router

  • Server-side rendering
  • API routes (optional proxy)
  • Authentication handling
  • Route protection

2. React Query

  • Server state management
  • Automatic caching
  • Background refetching
  • Optimistic updates
  • Cache invalidation on WebSocket events

3. Zustand Store

  • UI state (modals, filters, etc.)
  • WebSocket connection status
  • User preferences
  • Temporary UI state

4. WebSocket Client

  • Maintain connection to backend
  • Reconnect on disconnect
  • Trigger React Query invalidation
  • Handle incoming real-time events

MVP Features (Phase 1)

  • βœ… User authentication (Neon Auth)
  • βœ… Project management (create, list, view)
  • βœ… GitHub integration
    • Repository connection
    • Pull request listing
    • PR status (open, merged, closed)
    • Recent commits
    • Workflow run status
  • βœ… Real-time updates via WebSocket
  • βœ… Dashboard UI
    • Project overview
    • Activity feed
    • PR queue
    • Build status widgets

πŸ› Architecture Decisions

Authentication: Neon Auth

  • Decision: Use Neon Auth instead of GitHub OAuth for user authentication.
  • Reason: Simpler integration with the existing Neon database setup and streamlined user management within the Neon ecosystem.

πŸ— Architecture

Future Features (Phase 2+)

  • πŸ“… Deployment tracking (Vercel, Railway)
  • πŸ“… Infrastructure metrics (error rates, response times)
  • πŸ“… Team activity analytics
  • πŸ“… Custom webhook support
  • πŸ“… Slack/Discord notifications
  • πŸ“… Plugin system for custom integrations
  • πŸ“… Mobile app (React Native)

πŸ“ Project Structure

pulsar-dev/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ cmd/
β”‚   β”‚   └── server/
β”‚   β”‚       └── main.go              # Entry point
β”‚   β”œβ”€β”€ internal/
β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”œβ”€β”€ handlers/            # HTTP handlers
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ auth.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ projects.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ integrations.go
β”‚   β”‚   β”‚   β”‚   └── metrics.go
β”‚   β”‚   β”‚   β”œβ”€β”€ middleware/          # Auth, CORS, logging
β”‚   β”‚   β”‚   └── router.go            # Chi router setup
β”‚   β”‚   β”œβ”€β”€ websocket/
β”‚   β”‚   β”‚   β”œβ”€β”€ hub.go               # Connection manager
β”‚   β”‚   β”‚   β”œβ”€β”€ client.go            # Individual connection
β”‚   β”‚   β”‚   └── message.go           # Message types
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   β”œβ”€β”€ project.go           # Project business logic
β”‚   β”‚   β”‚   β”œβ”€β”€ integration.go       # Integration logic
β”‚   β”‚   β”‚   β”œβ”€β”€ metrics.go           # Metrics processing
β”‚   β”‚   β”‚   └── auth.go              # Auth logic
β”‚   β”‚   β”œβ”€β”€ workers/
β”‚   β”‚   β”‚   β”œβ”€β”€ github_poller.go     # GitHub API polling
β”‚   β”‚   β”‚   β”œβ”€β”€ coordinator.go       # Worker management
β”‚   β”‚   β”‚   └── publisher.go         # Redis publishing
β”‚   β”‚   β”œβ”€β”€ integrations/
β”‚   β”‚   β”‚   β”œβ”€β”€ github/              # GitHub API client
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ client.go
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ pulls.go
β”‚   β”‚   β”‚   β”‚   └── workflows.go
β”‚   β”‚   β”‚   └── registry.go          # Integration registry
β”‚   β”‚   β”œβ”€β”€ models/
β”‚   β”‚   β”‚   β”œβ”€β”€ project.go
β”‚   β”‚   β”‚   β”œβ”€β”€ user.go
β”‚   β”‚   β”‚   β”œβ”€β”€ integration.go
β”‚   β”‚   β”‚   └── metric.go
β”‚   β”‚   β”œβ”€β”€ database/
β”‚   β”‚   β”‚   β”œβ”€β”€ db.go                # Database connection
β”‚   β”‚   β”‚   └── queries.go           # SQL queries
β”‚   β”‚   └── config/
β”‚   β”‚       └── config.go            # Configuration
β”‚   β”œβ”€β”€ migrations/
β”‚   β”‚   β”œβ”€β”€ 000001_init_schema.up.sql
β”‚   β”‚   β”œβ”€β”€ 000001_init_schema.down.sql
β”‚   β”‚   β”œβ”€β”€ 000002_add_integrations.up.sql
β”‚   β”‚   └── 000002_add_integrations.down.sql
β”‚   β”œβ”€β”€ pkg/                         # Public packages (if any)
β”‚   β”œβ”€β”€ go.mod
β”‚   β”œβ”€β”€ go.sum
β”‚   β”œβ”€β”€ Dockerfile
β”‚   └── .env.example
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”‚   β”œβ”€β”€ layout.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx             # Landing page
β”‚   β”‚   β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx         # Dashboard home
β”‚   β”‚   β”‚   β”‚   └── [projectId]/
β”‚   β”‚   β”‚   β”‚       └── page.tsx     # Project detail
β”‚   β”‚   β”‚   └── api/                 # API routes (if needed)
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ ui/                  # shadcn/ui components
β”‚   β”‚   β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ProjectCard.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PRList.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ActivityFeed.tsx
β”‚   β”‚   β”‚   β”‚   └── MetricWidget.tsx
β”‚   β”‚   β”‚   └── layout/
β”‚   β”‚   β”‚       β”œβ”€β”€ Sidebar.tsx
β”‚   β”‚   β”‚       └── Header.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”œβ”€β”€ useWebSocket.ts      # WebSocket hook
β”‚   β”‚   β”‚   β”œβ”€β”€ useProjects.ts       # React Query hooks
β”‚   β”‚   β”‚   └── useAuth.ts
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ api.ts               # API client
β”‚   β”‚   β”‚   β”œβ”€β”€ websocket.ts         # WebSocket client
β”‚   β”‚   β”‚   └── utils.ts
β”‚   β”‚   └── store/
β”‚   β”‚       └── uiStore.ts           # Zustand store
β”‚   β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ package.json
β”‚   β”œβ”€β”€ tsconfig.json
β”‚   β”œβ”€β”€ tailwind.config.ts
β”‚   β”œβ”€β”€ next.config.js
β”‚   └── .env.local.example
β”œβ”€β”€ docker-compose.yml               # Local development
β”œβ”€β”€ .github/
β”‚   └── workflows/
β”‚       β”œβ”€β”€ backend-ci.yml
β”‚       └── frontend-ci.yml
β”œβ”€β”€ README.md
└── .gitignore

πŸ—„ Database Schema

Core Tables

users

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT UNIQUE NOT NULL,
    github_id TEXT UNIQUE,
    github_username TEXT,
    avatar_url TEXT,
    access_token TEXT,  -- Encrypted GitHub OAuth token
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_users_github_id ON users(github_id);
CREATE INDEX idx_users_email ON users(email);

projects

CREATE TABLE projects (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    description TEXT,
    owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_projects_owner ON projects(owner_id);

integrations

CREATE TABLE integrations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    type TEXT NOT NULL,  -- 'github', 'vercel', 'railway', etc.
    config JSONB NOT NULL,  -- { "repo": "owner/repo", "token": "..." }
    enabled BOOLEAN DEFAULT true,
    last_synced_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(project_id, type)  -- One integration of each type per project
);

CREATE INDEX idx_integrations_project ON integrations(project_id);
CREATE INDEX idx_integrations_type ON integrations(type);

pull_requests

CREATE TABLE pull_requests (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    integration_id UUID REFERENCES integrations(id) ON DELETE CASCADE,
    pr_number INTEGER NOT NULL,
    title TEXT NOT NULL,
    state TEXT NOT NULL,  -- 'open', 'closed', 'merged'
    author TEXT NOT NULL,
    author_avatar TEXT,
    url TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL,
    merged_at TIMESTAMPTZ,
    
    UNIQUE(integration_id, pr_number)
);

CREATE INDEX idx_prs_project ON pull_requests(project_id);
CREATE INDEX idx_prs_state ON pull_requests(state);
CREATE INDEX idx_prs_updated ON pull_requests(updated_at DESC);

workflow_runs

CREATE TABLE workflow_runs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    integration_id UUID REFERENCES integrations(id) ON DELETE CASCADE,
    run_id BIGINT NOT NULL,  -- GitHub's workflow run ID
    name TEXT NOT NULL,
    status TEXT NOT NULL,  -- 'queued', 'in_progress', 'completed'
    conclusion TEXT,  -- 'success', 'failure', 'cancelled', etc.
    workflow_name TEXT NOT NULL,
    branch TEXT,
    commit_sha TEXT,
    url TEXT NOT NULL,
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(integration_id, run_id)
);

CREATE INDEX idx_runs_project ON workflow_runs(project_id);
CREATE INDEX idx_runs_status ON workflow_runs(status);
CREATE INDEX idx_runs_started ON workflow_runs(started_at DESC);

metrics

-- Time-series metrics data
CREATE TABLE metrics (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    integration_id UUID REFERENCES integrations(id) ON DELETE CASCADE,
    metric_type TEXT NOT NULL,  -- 'pr_count', 'build_success_rate', etc.
    value JSONB NOT NULL,  -- Flexible storage for different metric shapes
    timestamp TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_metrics_project_type_time 
    ON metrics(project_id, metric_type, timestamp DESC);

-- Optional: Partition by month for better performance
-- CREATE TABLE metrics_2025_02 PARTITION OF metrics
--     FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');

project_members (for team features - future)

CREATE TABLE project_members (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    role TEXT NOT NULL DEFAULT 'member',  -- 'owner', 'admin', 'member'
    created_at TIMESTAMPTZ DEFAULT NOW(),
    
    UNIQUE(project_id, user_id)
);

CREATE INDEX idx_members_project ON project_members(project_id);
CREATE INDEX idx_members_user ON project_members(user_id);

Database Migrations

Use golang-migrate for schema management:

# Create new migration
migrate create -ext sql -dir migrations -seq add_deployments_table

# Apply migrations
migrate -path ./migrations -database "$DATABASE_URL" up

# Rollback
migrate -path ./migrations -database "$DATABASE_URL" down 1

πŸ”Œ API Endpoints

Authentication

POST   /api/auth/github                 # Initiate GitHub OAuth
GET    /api/auth/github/callback        # GitHub OAuth callback
POST   /api/auth/logout                 # Logout
GET    /api/auth/me                     # Get current user

Projects

GET    /api/projects                    # List user's projects
POST   /api/projects                    # Create new project
GET    /api/projects/:id                # Get project details
PATCH  /api/projects/:id                # Update project
DELETE /api/projects/:id                # Delete project

Integrations

GET    /api/projects/:id/integrations                # List project integrations
POST   /api/projects/:id/integrations                # Add integration
GET    /api/projects/:id/integrations/:type          # Get specific integration
PATCH  /api/projects/:id/integrations/:type          # Update integration
DELETE /api/projects/:id/integrations/:type          # Remove integration
POST   /api/projects/:id/integrations/:type/sync     # Trigger manual sync

Metrics & Data

GET    /api/projects/:id/pull-requests  # List PRs
GET    /api/projects/:id/workflows      # List workflow runs
GET    /api/projects/:id/metrics        # Get project metrics
GET    /api/projects/:id/activity       # Get activity feed

WebSocket

GET    /ws                               # WebSocket upgrade endpoint
                                         # Query params: ?token=<jwt_token>

Example Request/Response

Create Project:

POST /api/projects
Content-Type: application/json
Authorization: Bearer <jwt_token>

{
  "name": "My Awesome Project",
  "description": "Backend services for our app"
}

Response:

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "name": "My Awesome Project",
  "description": "Backend services for our app",
  "owner_id": "987fbc97-4bed-5078-9f07-9141ba07c9f3",
  "created_at": "2025-02-18T10:30:00Z",
  "updated_at": "2025-02-18T10:30:00Z"
}

Add GitHub Integration:

POST /api/projects/:id/integrations
Content-Type: application/json
Authorization: Bearer <jwt_token>

{
  "type": "github",
  "config": {
    "repo": "owner/repository-name"
  }
}

Response:

{
  "id": "456e7890-e89b-12d3-a456-426614174111",
  "project_id": "123e4567-e89b-12d3-a456-426614174000",
  "type": "github",
  "config": {
    "repo": "owner/repository-name"
  },
  "enabled": true,
  "created_at": "2025-02-18T10:35:00Z"
}

πŸ”΄ WebSocket Events

Connection

Client connects to /ws with JWT token:

const ws = new WebSocket('wss://api.pulsardev.io/ws?token=<jwt_token>');

Server β†’ Client Events

All events follow this structure:

interface WebSocketMessage {
  type: string;           // Event type
  project_id: string;     // Associated project
  data: any;              // Event-specific payload
  timestamp: number;      // Unix timestamp
}

Event Types

1. Pull Request Events

{
  "type": "pr_opened",
  "project_id": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "pr_number": 42,
    "title": "Add user authentication",
    "author": "johndoe",
    "url": "https://github.com/owner/repo/pull/42"
  },
  "timestamp": 1708257600
}

Event types: pr_opened, pr_closed, pr_merged, pr_updated

2. Workflow Events

{
  "type": "workflow_completed",
  "project_id": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "workflow_name": "CI",
    "status": "completed",
    "conclusion": "success",
    "url": "https://github.com/owner/repo/actions/runs/12345"
  },
  "timestamp": 1708257600
}

Event types: workflow_started, workflow_completed, workflow_failed

3. Metric Updates

{
  "type": "metric_update",
  "project_id": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "metric_type": "pr_count",
    "value": {
      "open": 5,
      "merged_today": 3
    }
  },
  "timestamp": 1708257600
}

4. Integration Sync

{
  "type": "integration_synced",
  "project_id": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "integration_type": "github",
    "items_updated": 15,
    "last_synced_at": "2025-02-18T10:45:00Z"
  },
  "timestamp": 1708257600
}

Client β†’ Server Events (Optional)

For future features like marking notifications as read:

{
  "type": "mark_read",
  "data": {
    "notification_id": "abc123"
  }
}

Heartbeat (Ping/Pong)

Server sends ping every 30 seconds. Client must respond with pong to maintain connection.


πŸš€ Setup & Installation

Prerequisites

  • Go 1.21+
  • Node.js 18+ and npm/yarn/pnpm
  • Docker & Docker Compose (for local development)
  • Neon account (free tier)
  • Upstash account (free tier) or local Redis
  • GitHub OAuth App credentials

1. Clone Repository

git clone https://github.com/MRQ67/pulsar-dev.git
cd pulsar-dev

2. Backend Setup

cd backend

# Install dependencies
go mod download

# Copy environment file
cp .env.example .env

# Edit .env with your credentials:
# - DATABASE_URL (Neon connection string)
# - REDIS_URL (Upstash or local Redis)
# - GITHUB_CLIENT_ID
# - GITHUB_CLIENT_SECRET
# - JWT_SECRET

# Run migrations
migrate -path ./migrations -database "$DATABASE_URL" up

# Start backend
go run cmd/server/main.go

Backend will run on http://localhost:8080

3. Frontend Setup

cd frontend

# Install dependencies
npm install  # or: yarn / pnpm install

# Copy environment file
cp .env.local.example .env.local

# Edit .env.local:
# NEXT_PUBLIC_API_URL=http://localhost:8080
# NEXT_PUBLIC_WS_URL=ws://localhost:8080

# Start development server
npm run dev

Frontend will run on http://localhost:3000

4. Local Development with Docker Compose

# From project root
docker-compose up -d

# This starts:
# - PostgreSQL (local, for dev)
# - Redis (local, for dev)
# - Backend (rebuilds on code changes)
# - Frontend (rebuilds on code changes)

5. GitHub OAuth Setup

  1. Go to GitHub Settings β†’ Developer settings β†’ OAuth Apps
  2. Create new OAuth App:
    • Application name: Pulsar Dev (Local)
    • Homepage URL: http://localhost:3000
    • Callback URL: http://localhost:8080/api/auth/github/callback
  3. Copy Client ID and Client Secret to backend .env

Environment Variables

Backend (.env):

# Server
PORT=8080
ENVIRONMENT=development

# Database
DATABASE_URL=postgresql://user:pass@host/db?sslmode=require

# Redis
REDIS_URL=redis://localhost:6379

# GitHub OAuth
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback

# JWT
JWT_SECRET=your-super-secret-key-change-this

# CORS
CORS_ORIGINS=http://localhost:3000

# Workers
GITHUB_POLL_INTERVAL=30  # seconds

Frontend (.env.local):

NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080

πŸ’» Development Workflow

Running Locally

Option 1: Manual (Recommended for development)

# Terminal 1: Backend
cd backend
go run cmd/server/main.go

# Terminal 2: Frontend
cd frontend
npm run dev

# Terminal 3: Redis (if not using Upstash)
redis-server

Option 2: Docker Compose

docker-compose up

Making Database Changes

  1. Create migration:
migrate create -ext sql -dir migrations -seq add_new_table
  1. Write up migration (migrations/000X_add_new_table.up.sql)
  2. Write down migration (migrations/000X_add_new_table.down.sql)
  3. Apply migration:
migrate -path ./migrations -database "$DATABASE_URL" up

Testing

Backend:

cd backend
go test ./...

# With coverage
go test -cover ./...

# Specific package
go test ./internal/services

Frontend:

cd frontend
npm test

# Watch mode
npm test -- --watch

Code Quality

Backend:

# Format
go fmt ./...

# Lint
golangci-lint run

# Vet
go vet ./...

Frontend:

# Format
npm run format

# Lint
npm run lint

# Type check
npm run type-check

🚒 Deployment

Backend (Railway)

  1. Create Railway project:
railway init
  1. Add Neon database:

    • Link existing Neon database
    • Or create new PostgreSQL instance
  2. Add Redis:

    • Use Upstash (recommended - serverless)
    • Or Railway Redis plugin
  3. Deploy backend:

railway up
  1. Set environment variables in Railway dashboard:
    • All variables from .env
    • Update GITHUB_CALLBACK_URL to production URL

Frontend (Vercel)

  1. Connect GitHub repository:

    • Import project on Vercel dashboard
    • Select frontend directory as root
  2. Configure build:

    • Framework Preset: Next.js
    • Build Command: npm run build
    • Output Directory: .next
  3. Set environment variables:

    • NEXT_PUBLIC_API_URL: Railway backend URL
    • NEXT_PUBLIC_WS_URL: Railway WebSocket URL (wss://)
  4. Deploy:

    • Automatic on push to main branch

Database Migrations in Production

Using Railway CLI:

railway run migrate -path ./migrations -database "$DATABASE_URL" up

Or use Neon branching:

  1. Create branch: migration-test
  2. Test migration on branch
  3. Merge to main if successful

CI/CD with GitHub Actions

Backend CI (.github/workflows/backend-ci.yml):

name: Backend CI

on:
  push:
    branches: [main, develop]
    paths:
      - 'backend/**'
  pull_request:
    paths:
      - 'backend/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Run tests
        working-directory: ./backend
        run: go test -v ./...

Frontend CI (.github/workflows/frontend-ci.yml):

name: Frontend CI

on:
  push:
    branches: [main, develop]
    paths:
      - 'frontend/**'
  pull_request:
    paths:
      - 'frontend/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        working-directory: ./frontend
        run: npm ci
      - name: Run tests
        working-directory: ./frontend
        run: npm test
      - name: Build
        working-directory: ./frontend
        run: npm run build

πŸ—Ί Roadmap

Phase 1: MVP (6-8 weeks)

  • Project architecture & planning
  • Backend foundation
    • Chi HTTP server
    • WebSocket Hub
    • Neon PostgreSQL connection
    • Redis pub/sub
  • GitHub integration
    • OAuth authentication
    • Repository connection
    • PR fetching
    • Workflow status
    • Background poller
  • Frontend dashboard
    • Authentication flow
    • Project list/create
    • Dashboard UI
    • Real-time updates
  • Deployment to Railway + Vercel

Phase 2: Enhanced Features (4-6 weeks)

  • Vercel integration (deployments)
  • Railway integration (deployments)
  • Team collaboration
    • Invite team members
    • Role-based permissions
  • Notifications
    • In-app notifications
    • Email notifications (optional)
  • Analytics
    • PR metrics (cycle time, review time)
    • Deployment frequency
    • Build success rates

Phase 3: Advanced Features (TBD)

  • Custom webhooks
  • Plugin system
  • Slack/Discord integration
  • API for third-party integrations
  • Mobile app (React Native)
  • Self-hosted version with Docker

GSoC Integration (March-April 2025)

  • Identify target organizations
  • Contribute to related open source projects
  • Submit GSoC proposal based on Pulsar Dev
  • Integrate Pulsar Dev with GSoC org's ecosystem

πŸ“š Key Implementation Details

Authentication Flow

  1. User clicks "Sign in with GitHub" on frontend
  2. Frontend redirects to /api/auth/github
  3. Backend redirects to GitHub OAuth
  4. User authorizes app
  5. GitHub redirects to /api/auth/github/callback
  6. Backend:
    • Exchanges code for access token
    • Fetches user info from GitHub
    • Creates/updates user in database
    • Generates JWT token
    • Redirects to frontend with JWT
  7. Frontend stores JWT in localStorage
  8. All subsequent requests include JWT in Authorization header

Worker Coordination

// Simplified coordinator example
type WorkerCoordinator struct {
    db          *sql.DB
    redis       *redis.Client
    workers     map[string]*Worker
    workerPool  chan struct{}  // Limit concurrent workers
}

func (c *WorkerCoordinator) Start() {
    // Fetch all active integrations
    integrations := c.fetchActiveIntegrations()
    
    // Start worker for each integration
    for _, integration := range integrations {
        go c.startWorker(integration)
    }
    
    // Monitor for new integrations
    ticker := time.NewTicker(1 * time.Minute)
    for range ticker.C {
        c.checkForNewIntegrations()
    }
}

func (c *WorkerCoordinator) startWorker(integration Integration) {
    worker := NewGitHubPoller(integration, c.redis)
    c.workers[integration.ID] = worker
    worker.Start()
}

Redis Pub/Sub Pattern

// Publisher (in worker)
func (w *GitHubPoller) publishEvent(eventType string, data interface{}) {
    event := Event{
        Type:      eventType,
        ProjectID: w.projectID,
        Data:      data,
        Timestamp: time.Now().Unix(),
    }
    
    payload, _ := json.Marshal(event)
    channel := fmt.Sprintf("project:%s:events", w.projectID)
    
    w.redis.Publish(ctx, channel, payload)
}

// Subscriber (in WebSocket server)
func subscribeToEvents(hub *Hub, redis *redis.Client) {
    pubsub := redis.Subscribe(ctx, "project:*:events")
    ch := pubsub.Channel()
    
    for msg := range ch {
        var event Event
        json.Unmarshal([]byte(msg.Payload), &event)
        
        // Broadcast to WebSocket clients
        hub.BroadcastToProject(event.ProjectID, event.Type, event.Data)
    }
}

React Query Integration

// hooks/useProjects.ts
export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: async () => {
      const res = await fetch('/api/projects', {
        headers: { Authorization: `Bearer ${getToken()}` }
      });
      return res.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

// hooks/useWebSocket.ts with React Query integration
export function useWebSocket() {
  const queryClient = useQueryClient();
  
  useEffect(() => {
    const ws = new WebSocket(WS_URL);
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      // Invalidate relevant queries on real-time updates
      if (message.type === 'pr_opened') {
        queryClient.invalidateQueries(['projects', message.project_id, 'prs']);
      }
      
      if (message.type === 'workflow_completed') {
        queryClient.invalidateQueries(['projects', message.project_id, 'workflows']);
      }
    };
    
    return () => ws.close();
  }, [queryClient]);
}

πŸŽ“ Learning Resources

Go Resources

Next.js Resources

Database Resources

Architecture Resources


🀝 Contributing

This is a personal learning project, but suggestions and feedback are welcome!

Development Principles

  1. Keep it simple - Don't over-engineer
  2. Ship fast - MVP over perfect
  3. Document as you go - Future you will thank you
  4. Test what matters - Core logic, not every line
  5. Learn by doing - Experiment and iterate

πŸ“ License

MIT License - feel free to use this for learning or building your own projects.


πŸ‘€ Author

Abdellah Qadi


πŸ™ Acknowledgments

Built as a learning project for GSoC 2025 preparation and contract work opportunities.

Special thanks to the Go, React, and open-source communities for excellent tools and documentation.


Ready to build? Start with the backend setup and let's ship this! πŸš€

About

"Your Projects, one dashboard." A real time DevOps System Monitor with a web dashboard and a backend in golang

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages