A modern, open-source tournament management system built with .NET 10 and SvelteKit. OpenTournament provides a complete solution for organizing, managing, and running competitive tournaments with support for various bracket formats.
- Tournament Management: Create and manage tournaments with multiple competitions and events
- Bracket Generation: Automatic bracket generation using single elimination format
- Real-time Updates: Live match updates and notifications via SignalR
- User Registration: Participant registration and management
- Match Management: Track match results, progression, and tournament standings
- Template System: Reusable tournament templates for quick setup
- REST API: Full-featured API with versioning and OpenAPI documentation
- Authentication: Firebase JWT authentication with Google OAuth support
- Observability: OpenTelemetry integration with Aspire Dashboard for monitoring
- .NET 10 - Modern, high-performance framework
- ASP.NET Core Minimal APIs - Lightweight, fast APIs
- Entity Framework Core - ORM with PostgreSQL
- Vertical Slice Architecture - Feature-focused code organization
- ErrorOr - Functional error handling
- MassTransit - Distributed messaging with RabbitMQ
- OpenTelemetry - Distributed tracing and metrics
- SvelteKit - Modern web framework with Svelte 5
- TypeScript - Type-safe JavaScript
- Tailwind CSS 4 - Utility-first CSS framework
- DaisyUI - Component library for Tailwind
- Auth.js - Authentication library
- PostgreSQL - Primary database
- Redis - Caching layer
- RabbitMQ - Message broker
- Docker - Containerization
- Aspire Dashboard - Development monitoring
- .NET 10 SDK
- Node.js 18+ (for frontend)
- Docker & Docker Compose (for infrastructure)
- PostgreSQL 16 (or use Docker Compose)
git clone https://github.com/CouchPartyGames/OpenTournament.git
cd OpenTournamentCreate an .env file in the extra directory:
cd extra
cat > .env << EOF
POSTGRES_PASSWORD=YourPassword
POSTGRES_USER=YourUser
POSTGRES_DB=tournament
REDIS_PASSWORD=YourPassword
EOFStart PostgreSQL, Redis, RabbitMQ, and Aspire Dashboard:
docker compose up -dServices will be available at:
- PostgreSQL:
localhost:5432 - Redis:
localhost:6379 - RabbitMQ Management:
http://localhost:15672 - Aspire Dashboard:
http://localhost:18888
Apply Entity Framework migrations:
cd ../src/OpenTournament.WebApi
dotnet ef database update --project ../OpenTournament.Corecd src/OpenTournament.WebApi
dotnet runThe API will be available at:
- HTTPS:
https://localhost:5001 - HTTP:
http://localhost:5000 - API Documentation:
https://localhost:5001/scalar/v1
cd src/OpenTournament.Frontend
npm install
npm run devThe frontend will be available at http://localhost:5173
OpenTournament/
├── src/
│ ├── OpenTournament.WebApi/ # API entry point (Minimal APIs)
│ ├── OpenTournament.Core/ # Core business logic (Vertical Slices)
│ └── OpenTournament.Frontend/ # SvelteKit frontend
├── tests/
│ ├── OpenTournament.Tests.Unit/ # Unit tests
│ └── OpenTournament.Tests.Integration/ # Integration tests
├── extra/
│ ├── docker-compose.yaml # Infrastructure services
│ └── opentournament.service # SystemD service file
├── Directory.Packages.props # Central package management
└── OpenTournament.slnx # Solution file
OpenTournament uses Vertical Slice Architecture where features are organized as self-contained slices:
OpenTournament.Core/Features/
├── Authentication/
│ ├── Login/
│ └── Register.cs
├── Competitions/
│ ├── Create/
│ │ ├── CreateCompetitionCommand.cs
│ │ └── CreateCompetitionHandler.cs
│ └── Get/
│ ├── GetCompetitionQuery.cs
│ ├── GetCompetitionResponse.cs
│ └── GetCompetitionHandler.cs
├── Tournaments/
│ ├── Create/
│ │ ├── CreateTournamentCommand.cs
│ │ ├── CreateTournamentValidator.cs
│ │ └── CreateTournamentHandler.cs
│ ├── Get/
│ ├── Update/
│ ├── Delete/
│ └── Start/
├── Events/
│ ├── Create/
│ └── Get/
├── Matches/
│ ├── Complete/
│ ├── Get/
│ └── Update/
├── Registration/
│ ├── Join/
│ ├── Leave/
│ └── List/
└── Templates/
├── Create/
├── Get/
├── Update/
├── Delete/
└── List/
Each feature slice contains:
- Commands/Queries
- Handlers
- Validators (optional, using FluentValidation)
- Response DTOs
- Business logic
The command defines the input data for creating a tournament:
// CreateTournamentCommand.cs
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using OpenTournament.Core.Domain.Entities;
namespace OpenTournament.Core.Features.Tournaments.Create;
public sealed record CreateTournamentCommand(
[Required]
[property: Description("name of the tournament")]
string Name,
[property: Description("start time of the tournament")]
DateTime StartTime,
[property: Description("minimum number of users required to start")]
int MinParticipants,
[property: Description("maximum number of users allowed to join")]
int MaxParticipants,
[property: Description("single or double elimination tournament")]
EliminationMode Mode,
[property: Description("seed players randomly or by rank")]
DrawSeeding Seeding
);FluentValidation provides complex validation rules:
// CreateTournamentValidator.cs
using FluentValidation;
namespace OpenTournament.Core.Features.Tournaments.Create;
public sealed class CreateTournamentValidator : AbstractValidator<CreateTournamentCommand>
{
public CreateTournamentValidator()
{
RuleFor(c => c.Name)
.Length(3, 50)
.NotEmpty();
RuleFor(c => c.StartTime)
.GreaterThan(DateTime.Now);
RuleFor(c => c.MinParticipants)
.GreaterThan(2);
RuleFor(c => c.MaxParticipants)
.GreaterThan(c => c.MinParticipants)
.LessThanOrEqualTo(256);
RuleFor(c => c.Mode)
.IsInEnum();
RuleFor(c => c.Seeding)
.IsInEnum();
}
}The handler processes the command and returns ErrorOr<T>:
// CreateTournamentHandler.cs
using ErrorOr;
using FluentValidation.Results;
using OpenTournament.Core.Domain.Entities;
using OpenTournament.Core.Domain.ValueObjects;
using OpenTournament.Core.Infrastructure.Persistence;
namespace OpenTournament.Core.Features.Tournaments.Create;
public static class CreateTournamentHandler
{
public static async Task<ErrorOr<Created>> HandleAsync(
CreateTournamentCommand command,
string userId,
AppDbContext dbContext,
CancellationToken ct)
{
// Validate the command
CreateTournamentValidator validator = new();
ValidationResult validationResult = await validator.ValidateAsync(command, ct);
if (!validationResult.IsValid)
{
return Error.Validation();
}
// Create the tournament entity
var tournament = new Tournament
{
Id = TournamentId.NewTournamentId(),
Name = command.Name,
Creator = Creator.New(new ParticipantId(userId))
};
// Persist to database
await dbContext.AddAsync(tournament, ct);
await dbContext.SaveChangesAsync(ct);
return Result.Created;
}
}For read operations, define a query and response:
// GetCompetitionQuery.cs
namespace OpenTournament.Core.Features.Competitions.Get;
public record GetCompetitionQuery();
// GetTournamentResponse.cs
using OpenTournament.Core.Domain.Entities;
namespace OpenTournament.Core.Features.Tournaments.Get;
public sealed record GetTournamentResponse(Tournament Tournament);
// GetTournamentHandler.cs
using ErrorOr;
using Microsoft.EntityFrameworkCore;
using OpenTournament.Core.Domain.ValueObjects;
using OpenTournament.Core.Infrastructure.Persistence;
namespace OpenTournament.Core.Features.Tournaments.Get;
public static class GetTournamentHandler
{
public static async Task<ErrorOr<GetTournamentResponse>> HandleAsync(
string id,
AppDbContext dbContext,
CancellationToken token)
{
var tournamentId = TournamentId.TryParse(id);
if (tournamentId is null)
{
return Error.Validation();
}
var tournament = await dbContext
.Tournaments
.Include(m => m.Matches)
.FirstOrDefaultAsync(m => m.Id == tournamentId, token);
if (tournament is null)
{
return Error.NotFound();
}
return new GetTournamentResponse(tournament);
}
}See src/OpenTournament.Core/README.md for detailed architecture documentation.
# Build the solution
dotnet build
# Run tests
dotnet test
# Run unit tests only
dotnet test tests/OpenTournament.Tests.Unit
# Run integration tests only
dotnet test tests/OpenTournament.Tests.Integration
# Create a new migration
cd src/OpenTournament.Core
dotnet ef migrations add MigrationName --startup-project ../OpenTournament.WebApi
# Apply migrations
cd src/OpenTournament.WebApi
dotnet ef database update --project ../OpenTournament.Corecd src/OpenTournament.Frontend
# Run development server
npm run dev
# Type-check
npm run check
# Format code
npm run format
# Lint
npm run lint
# Build for production
npm run buildWhen running in development mode, API documentation is available via Scalar:
- Scalar UI:
https://localhost:5001/scalar/v1 - OpenAPI spec:
https://localhost:5001/openapi/v1.json
Unit tests use xUnit, FluentAssertions, and NSubstitute:
dotnet test tests/OpenTournament.Tests.UnitIntegration tests use Testcontainers for real PostgreSQL instances:
dotnet test tests/OpenTournament.Tests.Integration- FluentAssertions - Readable assertions
- NSubstitute - Mocking framework
- Testcontainers - Docker containers for integration tests
- Bogus - Test data generation
- Respawn - Database cleanup between tests
The extra/docker-compose.yaml file provides all required infrastructure services.
To start services:
cd extra
docker compose up -dTo stop services:
docker compose downTo view logs:
docker compose logs -fFor production deployments on Linux, a SystemD service file is provided at extra/opentournament.service.
- Copy the service file:
sudo cp extra/opentournament.service /etc/systemd/system/-
Edit the service file to match your installation paths
-
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now opentournament.service- Check service status:
sudo systemctl status opentournament.serviceThe API uses URL-based versioning:
- Version 1:
/tournaments/v1/,/events/v1/,/matches/v1/ - Version 2:
/tournaments/v2/(when available)
All endpoints are versioned to maintain backward compatibility.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow Vertical Slice Architecture for new features
- Use ErrorOr for error handling (OneOf is deprecated)
- Use strongly-typed IDs for domain entities
- Write unit tests for business logic
- Write integration tests for API endpoints
- Follow existing code style and conventions
See CLAUDE.md for detailed development guidance.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for a list of changes and version history.
For issues, questions, or contributions, please visit the GitHub repository.
Built with ❤️ by CouchPartyGames