A production-ready leaderboard engine built with Redis, with real-time updates over WebSocket and gRPC streaming.
-
๐ 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
- Redis Sorted Sets (
-
๐ฏ Developer-First
- Clean architecture (
core,repo,delivery,auth) - Unit and integration tests
- gRPC + WebSocket support out of the box
- Clean architecture (
-
๐ Tooling
- GitHub Actions CI
- Codecov integration
- Docker and Docker Compose setup
- Integration Methods
- Quick Start
- Architecture
- API Reference
- Configuration
- Development
- Testing
- CI and Release
- Deployment
- Troubleshooting
- Contributing
GoBoard can be integrated in two ways:
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
Run GoBoard as a separate process and connect via WebSocket/gRPC.
Pros:
- โ Language-agnostic API
- โ Independent scaling
- โ Shared leaderboard backend for multiple services
1. Install the module
go get github.com/jassus213/go-board/dashboard2. 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)
}
}1. Clone and configure
git clone https://github.com/jassus213/go-board.git
cd go-board
cp cmd/dashboard-server/env.example .env2. Start dependencies (Redis + Redis Insight)
docker compose -f deploy/docker/local/docker-compose.yml up -d3. Run the server
AUTH_SECRET=super-secret-key go run ./cmd/dashboard-server/main.go --ws --grpcService 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/StreamUpdatescURL (Quick Test)
curl http://localhost:8080/healthgo-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
- 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
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=trueRuntime now consumes AUTH_MODE, AUTH_SECRET, and JWT_SECRET.
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"
}
}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.), ProblemDetailsattached to statusdetailsfor typed client-side handling.
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/incrementwith body{"increment": 10.5}GET /dashboards/:dashboard/members/:member_id/rankGET /dashboards/:dashboard/top?limit=10GET /dashboards/:dashboard/stats
Errors are returned as application/problem+json payloads using ProblemDetails.
Set shared variables:
TOKEN=super-secret-key
BASE_URL=http://localhost:8080/api/v1
GRPC_ADDR=localhost:50051REST (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/GetDashboardStatsError 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"
}- Runtime in
cmd/dashboard-serversupportsAUTH_MODE=static|jwt|noop. - In
staticmode, valid token resolves to fixed"admin_user". - In
jwtmode, identity is extracted from JWTuser_idclaim. noopmode is only for local development/testing and should not be used in production.- Incoming
member_idis validated/overridden by authenticated identity logic in transport handlers.
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
}# Start dependencies
docker compose -f deploy/docker/local/docker-compose.yml up -d
# Run service
go run ./cmd/dashboard-server/main.goThis repository currently has a two-module Go setup:
- root module (
go.mod) fordashboard-api(service runtime), - nested module (
dashboard/go.mod) for the reusabledashboardpackage.
# 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.goBuild 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:latestdocker compose -f deploy/docker/local/docker-compose.yml up -dgo test ./...
(cd dashboard && go test ./...)(cd dashboard && go test -coverprofile=../coverage.out -covermode=atomic ./...)(cd dashboard && go test -bench=. -benchmem ./auth/...)- CI runs from
.github/workflows/ci.yml(lint, tests, build, docker, benchmark). - Releases run from
.github/workflows/release.ymlon tagsv*.*.*. - For daily development, badges at the top of this README are the primary status view.
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes
- Run tests locally
- Open a Pull Request
- Follow standard Go conventions
- Run
gofmtbefore committing - Add tests for new features
- Update docs when behavior changes
For an expanded contributor checklist, see CONTRIBUTING.md.
- Redis connection errors
- Ensure Redis is running on
REDIS_ADDR(defaultlocalhost:6379). - Quick check:
redis-cli -h localhost -p 6379 ping.
- Ensure Redis is running on
- 401/403 on WebSocket/gRPC
- Ensure token format matches
AUTH_MODE:static: token must equalAUTH_SECRETjwt: token must be a valid JWT signed withJWT_SECRETand includeuser_id
- For gRPC, include
authorizationmetadata (Bearer <token>or plain token).
- Ensure token format matches
- Browser WS rejected by CORS
- Add your frontend origin to
CORS_ALLOWED_ORIGINS. - Do not use wildcard
*in production.
- Add your frontend origin to
This project is licensed under the MIT License - see the LICENSE file for details.
Nikita Okhotnikov
- GitHub: @jassus213
- Uber Fx - dependency injection and lifecycle
- go-redis - Redis client for Go
- Gorilla WebSocket - WebSocket implementation
- gRPC - RPC framework
- Redis - sorted-set storage model
For issues and questions:
Built with โค๏ธ using Go and Redis