Leonard Surac

Back to the start page

Live · since 2026

Website and club software of the Sulzfeld chess club

My chess club had an outdated website and organised everything through chats and private mailboxes. I built it a website with an editorial system, a members area and match-day planning, and I run it.

ASP.NET Core 10 · EF Core · Angular 22 SSR · PostgreSQL · Node.js · Docker · Caddy

Problem

My club's website was outdated and only one person could maintain it. League results were typed in by hand, newsletters went out from private mailboxes and line-ups were pieced together in chat groups. A lot of work for volunteers, and none of it visible to visitors.

My role

I designed, built and launched the solution on my own, and I still run it.

Case study

My solution

I wanted club work to happen where the data lives: an editorial system for the board, a members area for submissions and an import that fetches whatever other systems already know.

  • Results, ratings and venues are imported every night from nuLiga and the chess federation
  • Scoresheets become PGN from a photo: an AI transcribes, Stockfish checks, a human decides open moves
  • Members submit articles, match reports and newsletters, and the board approves them
  • Match-day planning suggests line-ups and collects availability and car pools

Implementation

I built the site with Angular and server-side rendering so it loads fast and gets found, plus an ASP.NET Core API on PostgreSQL and a separate Node worker for the import. Because the software processes member data, I started with everything locked: every endpoint requires an admin, and I open member and guest areas on purpose.

SkSulzfeld.Api/Program.cs
builder.Services.AddAuthorization(options =>
{
  var adminOnly = new AuthorizationPolicyBuilder()
      .RequireAuthenticatedUser()
      .RequireRole(nameof(UserRole.Admin))
      .Build();
  options.DefaultPolicy = adminOnly;
  options.FallbackPolicy = adminOnly;
  options.AddPolicy(AuthorizationPolicies.MemberArea, policy => policy
      .RequireAuthenticatedUser()
      .RequireRole(nameof(UserRole.Admin), nameof(UserRole.Member)));
});

Operations

I deploy with GitHub Actions and a database dump before every rollout, test on a staging copy in my homelab first and back up every night, encrypted and off-site. Once a month an automated restore drill restores the backup as a proof. The repository is private because it processes member data.

Decisions

Everything is closed until I open it
I decided against a fine-grained role matrix: without an explicit opening every endpoint needs an admin, and a test freezes the public surface. A forgotten attribute locks people out instead of exposing data.
I do not copy personal data
Member data writes no revisions, and newsletter logs point to the member instead of copying the address. Nobody lives on in a history after leaving; the price is no change history for them.
Nobody gets a newsletter twice
I claim every recipient in a single database statement before sending, so two parallel requests can never mail anyone twice. If the process dies exactly in between, one recipient stays marked as sent.

Outcome

The site has been live since September 2026. The board maintains content itself, members can submit reports through the members area, and results and ratings arrive every night on their own. Behind it are 26 domains with around 1,400 backend and 2,000 front-end tests.