Skip to content

jassus213/go-board

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

15 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ† GoBoard - Real-Time Leaderboard Engine

Go Version CI codecov Go Report Card License Release

A production-ready leaderboard engine built with Redis, with real-time updates over WebSocket and gRPC streaming.

โœจ Key Features

  • ๐Ÿ”Œ Easy Integration

    • Import as a Go module
    • Run as a standalone service
    • Clear interfaces for custom implementations
  • โšก Performance-Oriented

    • Redis Sorted Sets (O(log N))
    • Batch operations with pipelining
    • Lightweight runtime and transport layers
  • ๐ŸŽฏ Developer-First

    • Clean architecture (core, repo, delivery, auth)
    • Unit and integration tests
    • gRPC + WebSocket support out of the box
  • ๐Ÿš€ Tooling

    • GitHub Actions CI
    • Codecov integration
    • Docker and Docker Compose setup

๐Ÿ“‹ Table of Contents

๐Ÿ”Œ Integration Methods

GoBoard can be integrated in two ways:

Method 1: As a Go Module (Recommended)

Import dashboard into your Go app and use the repository/use case directly.

Pros:

  • โœ… Full control over integration
  • โœ… No network hop
  • โœ… Easy extension inside your service

Method 2: As a Standalone Service

Run GoBoard as a separate process and connect via WebSocket/gRPC.

Pros:

  • โœ… Language-agnostic API
  • โœ… Independent scaling
  • โœ… Shared leaderboard backend for multiple services

๐Ÿš€ Quick Start

Option A: Use as a Go Module

1. Install the module

go get github.com/jassus213/go-board/dashboard

2. Minimal usage

package main

import (
	"context"
	"fmt"

	"github.com/jassus213/go-board/dashboard/repo/redis"
	goredis "github.com/redis/go-redis/v9"
)

func main() {
	ctx := context.Background()
	rdb := goredis.NewClient(&goredis.Options{Addr: "localhost:6379"})
	repo := redis.NewDashboardRedisRepository(rdb, "myapp:")

	if err := repo.AddMemberToDashboard(ctx, "global", "user123", 1000); err != nil {
		panic(err)
	}

	top, _ := repo.GetTopMembers(ctx, "global", 10)
	for _, member := range top {
		fmt.Printf("Rank %d: %s (%.0f points)\n", member.Rank, member.ID, member.Score)
	}
}

Option B: Run as a Standalone Service

1. Clone and configure

git clone https://github.com/jassus213/go-board.git
cd go-board
cp cmd/dashboard-server/env.example .env

2. Start dependencies (Redis + Redis Insight)

docker compose -f deploy/docker/local/docker-compose.yml up -d

3. Run the server

AUTH_SECRET=super-secret-key go run ./cmd/dashboard-server/main.go --ws --grpc

Service endpoints:

  • WebSocket: ws://localhost:8080/ws
  • gRPC: localhost:50051
  • Health: http://localhost:8080/health

4. Smoke test transports

JavaScript/TypeScript (WebSocket)

const ws = new WebSocket('ws://localhost:8080/ws?token=super-secret-key');

ws.onopen = () => {
    ws.send(JSON.stringify({
        dashboard: "global_leaderboard",
        member_id: "user123",
        increment: 10.5
    }));
};

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('New rank:', data.rank, 'error:', data.error);
};

gRPC (grpcurl)

grpcurl -plaintext \
  -import-path dashboard/delivery/grpc/proto \
  -proto dashboard.proto \
  -H "authorization: Bearer super-secret-key" \
  -d '{"dashboard":"global","member_id":"user123","increment":10}' \
  localhost:50051 dashboard.DashboardService/StreamUpdates

cURL (Quick Test)

curl http://localhost:8080/health

๐Ÿ—๏ธ Architecture

go-board/
โ”œโ”€โ”€ cmd/dashboard-server/             # service entrypoint (dashboard-api module)
โ”œโ”€โ”€ dashboard/                        # reusable leaderboard module
โ”‚   โ”œโ”€โ”€ auth/                         # token verifiers
โ”‚   โ”œโ”€โ”€ core/                         # dto, entities, interfaces, usecase
โ”‚   โ”œโ”€โ”€ delivery/
โ”‚   โ”‚   โ”œโ”€โ”€ grpc/                     # gRPC server + proto/gen
โ”‚   โ”‚   โ””โ”€โ”€ ws/                       # WebSocket hub/clients
โ”‚   โ””โ”€โ”€ repo/redis/                   # Redis repository implementation
โ””โ”€โ”€ deploy/docker/                    # Docker assets

Key Design Patterns

  • Use Case Layer: business workflows are orchestrated in dashboard/core/usecase
  • Repository Pattern: data access behind interfaces
  • Hub Pattern: WebSocket connection management
  • Dependency Injection: Uber Fx runtime composition

โš™๏ธ Configuration

Environment Variables

The runtime (cmd/dashboard-server/main.go) reads .env if present.

# Server
HTTP_PORT=:8080
GRPC_PORT=:50051

# Redis
REDIS_ADDR=localhost:6379
REDIS_PASS=

# Auth mode: static, jwt, noop
AUTH_MODE=static

# Static mode secret (used when AUTH_MODE=static)
AUTH_SECRET=super-secret-key

# JWT mode secret (used when AUTH_MODE=jwt)
JWT_SECRET=your-jwt-secret

# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000
CORS_ALLOW_CREDENTIALS=false

# Transport toggles
ENABLE_WEBSOCKET=true
ENABLE_GRPC=true

Runtime now consumes AUTH_MODE, AUTH_SECRET, and JWT_SECRET.

๐Ÿ“ก API Reference

WebSocket API

Connect

ws://localhost:8080/ws?token=<TOKEN>

Send Score Update

{
  "dashboard": "global_leaderboard",
  "member_id": "user123",
  "increment": 10.5
}

Receive Response

{
  "rank": 42
}

On error, WebSocket responses include typed Problem Details:

{
  "problem": {
    "type": "urn:goboard:request:invalid-argument",
    "title": "Bad Request",
    "status": 400,
    "detail": "missing dashboard or member_id",
    "instance": "/ws",
    "code": "invalid_argument"
  }
}

gRPC API

Service Definition

service DashboardService {
  rpc StreamUpdates (stream UpdateRequest) returns (stream UpdateResponse);
  rpc IncrementScore (IncrementScoreRequest) returns (IncrementScoreResponse);
  rpc GetMemberRank (GetMemberRankRequest) returns (GetMemberRankResponse);
  rpc GetTopMembers (GetTopMembersRequest) returns (GetTopMembersResponse);
  rpc GetDashboardStats (GetDashboardStatsRequest) returns (GetDashboardStatsResponse);
}

message UpdateRequest {
  string dashboard = 1;
  string member_id = 2;
  double increment = 3;
}

message UpdateResponse {
  string member_id = 1;
  int64 rank = 2;
  double score = 3;
  ProblemDetails problem = 5;
}

message ProblemDetails {
  string type = 1;
  string title = 2;
  int32 status = 3;
  string detail = 4;
  string instance = 5;
  string code = 6;
}

message IncrementScoreRequest {
  string dashboard = 1;
  string member_id = 2;
  double increment = 3;
}

message IncrementScoreResponse {
  string member_id = 1;
  int64 rank = 2;
}

Auth failures in the gRPC interceptor are returned as grpc status errors with:

  • code mapped from ProblemDetails.status (Unauthenticated, PermissionDenied, etc.),
  • ProblemDetails attached to status details for typed client-side handling.

REST API (Gin)

All REST routes are under http://localhost:8080/api/v1 and require Authorization: Bearer <TOKEN>. <TOKEN> depends on AUTH_MODE (AUTH_SECRET for static, JWT for jwt).

  • POST /dashboards/:dashboard/members/:member_id/increment with body {"increment": 10.5}
  • GET /dashboards/:dashboard/members/:member_id/rank
  • GET /dashboards/:dashboard/top?limit=10
  • GET /dashboards/:dashboard/stats

Errors are returned as application/problem+json payloads using ProblemDetails.

API Cheat Sheet

Set shared variables:

TOKEN=super-secret-key
BASE_URL=http://localhost:8080/api/v1
GRPC_ADDR=localhost:50051

REST (curl)

# Increment authenticated member score
curl -X POST "$BASE_URL/dashboards/global/members/user123/increment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"increment":10.5}'

# Get authenticated member rank
curl -X GET "$BASE_URL/dashboards/global/members/user123/rank" \
  -H "Authorization: Bearer $TOKEN"

# Get top members
curl -X GET "$BASE_URL/dashboards/global/top?limit=5" \
  -H "Authorization: Bearer $TOKEN"

# Get dashboard stats
curl -X GET "$BASE_URL/dashboards/global/stats" \
  -H "Authorization: Bearer $TOKEN"

gRPC unary (grpcurl)

# Increment score
grpcurl -plaintext \
  -import-path dashboard/delivery/grpc/proto \
  -proto dashboard.proto \
  -H "authorization: Bearer $TOKEN" \
  -d '{"dashboard":"global","member_id":"user123","increment":10.5}' \
  $GRPC_ADDR dashboard.DashboardService/IncrementScore

# Get member rank
grpcurl -plaintext \
  -import-path dashboard/delivery/grpc/proto \
  -proto dashboard.proto \
  -H "authorization: Bearer $TOKEN" \
  -d '{"dashboard":"global","member_id":"user123"}' \
  $GRPC_ADDR dashboard.DashboardService/GetMemberRank

# Get top members
grpcurl -plaintext \
  -import-path dashboard/delivery/grpc/proto \
  -proto dashboard.proto \
  -H "authorization: Bearer $TOKEN" \
  -d '{"dashboard":"global","limit":5}' \
  $GRPC_ADDR dashboard.DashboardService/GetTopMembers

# Get dashboard stats
grpcurl -plaintext \
  -import-path dashboard/delivery/grpc/proto \
  -proto dashboard.proto \
  -H "authorization: Bearer $TOKEN" \
  -d '{"dashboard":"global"}' \
  $GRPC_ADDR dashboard.DashboardService/GetDashboardStats

Error example (ProblemDetails)

curl -i -X GET "$BASE_URL/dashboards/global/stats"
{
  "type": "urn:goboard:auth:missing-token",
  "title": "Unauthorized",
  "status": 401,
  "detail": "authentication token is required",
  "instance": "/api/v1/dashboards/global/stats",
  "code": "auth_missing_token"
}

Known Runtime Behavior

  • Runtime in cmd/dashboard-server supports AUTH_MODE=static|jwt|noop.
  • In static mode, valid token resolves to fixed "admin_user".
  • In jwt mode, identity is extracted from JWT user_id claim.
  • noop mode is only for local development/testing and should not be used in production.
  • Incoming member_id is validated/overridden by authenticated identity logic in transport handlers.

Repository Interface

type DashboardRepository interface {
    AddMemberToDashboard(ctx context.Context, dashboard, member string, score float64) error
    AddMembersBatch(ctx context.Context, dashboard string, members []entity.DashboardRecord) error
    RemoveMemberFromDashboard(ctx context.Context, dashboard, member string) error
    GetTopMembers(ctx context.Context, dashboard string, top int64) ([]entity.DashboardRecord, error)
    ViewMemberRank(ctx context.Context, dashboard string, memberId string) (int64, error)
    IncrementMemberScore(ctx context.Context, dashboard, member string, increment float64) error
    GetTotalMembers(ctx context.Context, dashboard string) (int64, error)
    DeleteDashboard(ctx context.Context, dashboard string) error
    IncrementMembersBatch(ctx context.Context, dashboard string, increments []entity.DashboardRecord) error
}

๐Ÿ› ๏ธ Development

Running Locally

# Start dependencies
docker compose -f deploy/docker/local/docker-compose.yml up -d

# Run service
go run ./cmd/dashboard-server/main.go

This repository currently has a two-module Go setup:

  • root module (go.mod) for dashboard-api (service runtime),
  • nested module (dashboard/go.mod) for the reusable dashboard package.

Useful Commands

# Root module tests (dashboard-api)
go test ./...

# Library module tests
(cd dashboard && go test ./...)

# Build server binary
go build -o bin/dashboard-server ./cmd/dashboard-server/main.go

๐Ÿณ Deployment

Using Docker

Build Image

docker build -t goboard:latest -f deploy/docker/Dockerfile .

Run Container

docker run -d \
  --name goboard \
  -p 8080:8080 \
  -p 50051:50051 \
  -e REDIS_ADDR=host.docker.internal:6379 \
  -e AUTH_SECRET=your-secret \
  goboard:latest

Using Docker Compose (Dependencies)

docker compose -f deploy/docker/local/docker-compose.yml up -d

๐Ÿงช Testing

Run All Tests

go test ./...
(cd dashboard && go test ./...)

Run with Coverage

(cd dashboard && go test -coverprofile=../coverage.out -covermode=atomic ./...)

Benchmarks

(cd dashboard && go test -bench=. -benchmem ./auth/...)

๐Ÿ”„ CI and Release

  • CI runs from .github/workflows/ci.yml (lint, tests, build, docker, benchmark).
  • Releases run from .github/workflows/release.yml on tags v*.*.*.
  • For daily development, badges at the top of this README are the primary status view.

๐Ÿค Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes
  4. Run tests locally
  5. Open a Pull Request

Code Style

  • Follow standard Go conventions
  • Run gofmt before committing
  • Add tests for new features
  • Update docs when behavior changes

For an expanded contributor checklist, see CONTRIBUTING.md.

๐Ÿ›Ÿ Troubleshooting

  • Redis connection errors
    • Ensure Redis is running on REDIS_ADDR (default localhost:6379).
    • Quick check: redis-cli -h localhost -p 6379 ping.
  • 401/403 on WebSocket/gRPC
    • Ensure token format matches AUTH_MODE:
      • static: token must equal AUTH_SECRET
      • jwt: token must be a valid JWT signed with JWT_SECRET and include user_id
    • For gRPC, include authorization metadata (Bearer <token> or plain token).
  • Browser WS rejected by CORS
    • Add your frontend origin to CORS_ALLOWED_ORIGINS.
    • Do not use wildcard * in production.

๐Ÿ“ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ‘ค Author

Nikita Okhotnikov

๐Ÿ™ Acknowledgments

๐Ÿ“ฎ Support

For issues and questions:


Built with โค๏ธ using Go and Redis

About

High-performance real-time leaderboard service in Go + Redis with gRPC bidirectional streaming and WebSockets.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages