Home/ Work/SD Radovljica
Lead dev · 2026 · Live

SD Radovljica

A custom member portal for a Slovenian shooting club — training registrations, competition management, season rankings, and iCal feeds, built as a bespoke WordPress plugin with fintech-grade audit trails.

Role Lead Developer
Year 2026
Platform WordPress plugin + public website
Stack PHP 8.1 · WordPress · MySQL · WP-Cron
Domain Sports club · Member management
SD Radovljica screenshot
Custom DB tables
10
disciplines, sessions, registrations, results, rankings, documents, audit log
Role tiers
4
member · competition admin · board member · administrator
Plugin capabilities
14
granular permission model
Email automations
3
confirmations, results, daily reminders

Overview

Most sports clubs manage members in spreadsheets. SD Radovljica — a shooting club in Radovljica, Slovenia — wanted something better: a proper member portal where every registered member can browse the training calendar, sign up for sessions, enter competitions, and see their season standings, all within a secure, role-gated system.

I built it as a custom WordPress plugin — not a theme hack or a pile of shortcodes, but a first-class PSR-4 PHP package with a layered architecture (Models → Repositories → Services → API / Admin / Frontend), a full PHPUnit test suite with Brain Monkey and wp-phpunit, static analysis via PHPStan, and WordPress Coding Standards enforced in CI.

The public website is a GeneratePress child theme ported from approved HTML mockups. The differentiating work is the plugin: a self-contained member management system that would cost five figures as an off-the-shelf SaaS but runs inside a €10/month cPanel host — because every piece of it was built to exactly what the club needs.

Architecture

~/sd-radovljica/architecture/system-map.svg
01 — CLIENT SURFACESPublic WebsiteGeneratePress themeMember Portalshortcodes · frontendAdmin PanelWP admin · 7 pagesiCal Feedspublic + personalEmail / CronWP-Cron · wp_mailSD Radovljica PluginPHP 8.1 · PSR-4 · WordPress plugin v1.4.0Models → Repositories → Services → API / Admin / Frontend4 roles · 14 capabilities · full audit log02 — DATA LAYERMembersWP users + rolesSessionstrainings · eventsCompetitionsslots · registrationsResultsscores · rankingsDocuments3 access tiersAudit Logevery mutationMySQL · cPanel hosting · SFTP deploy · WP-Cron · GDPR export/erase · PHPUnit CI03 — INFRASTRUCTURE

Reading the diagram: Five surfaces — the public GeneratePress website, the member-facing portal (shortcodes), the WordPress admin panel (7 management pages), iCal feeds, and the WP-Cron email pipeline — all talk to a single plugin core. Ten custom database tables handle the full data model: members are native WordPress users extended with four custom roles and 14 capabilities. Every write that matters — registration, result entry, slot assignment, cancellation — is logged to the audit table with IP address and user-agent.

The member portal in detail

The portal has four role tiers, each with explicit capabilities set at activation time. An SDR Member (SDR Član) can register for training sessions and competitions, view results, and see their own registrations. An SDR Competition Admin adds the ability to manage sessions, manage competitions, enter results, and assign slots. An SDR Board Member (SDR Član IO) gets read access to board documents — meeting minutes, assembly records, rules — which are invisible to ordinary members. Administrators have all 14 capabilities.

Training calendar. Sessions are stored in a custom table with discipline, event type (training, competition, dynamics session, visit, other), location, capacity, and optional recurrence (weekly, biweekly, monthly). Members register through a shortcode-rendered frontend; capacity enforcement and schedule conflict detection happen server-side. Registration timestamps are stored to microsecond precision (datetime(6)) — when a session fills at the exact same second, the earlier microsecond wins, giving a deterministic and fair queue ordering without application-level locks.

Competition management. Competitions have an explicit lifecycle: draft → registration open → registration closed → in progress → completed → cancelled. Members submit up to three time-slot preferences; admins assign a confirmed slot and lane number through an AJAX admin interface. Status transitions — pending, confirmed, DNS, DNF — track every stage of a member’s competition journey.

Season rankings. Every competition result carries a score, category (junior / senior / veteran), and an is_ranked flag. A configurable best-of-N rule (default: best 5 of 10 per season) drives the Ranking_Calculator. Instead of a cron job that recomputes everything on a schedule, rankings rebuild synchronously the moment a result is saved — the sdr_result_saved action fires inside the repository, triggers the calculator scoped to that (discipline_id, season_year) pair, and the leaderboard is current before the HTTP response returns. Dense-rank tie-breaking (1,1,3 rather than 1,2,3) matches how shooting results are conventionally presented.

iCal integration. The plugin generates RFC 5545-compliant .ics feeds — one public feed per discipline or event type, and one personal feed per member (their own registrations only). Personal feeds are protected by a 32-character token stored in user meta; the URL pattern is /sdr-calendar/personal/<user_id>/<token>/. Members subscribe once from Google Calendar or Apple Calendar and stay in sync automatically.

Email automation. Three automated paths: registration confirmation on sign-up (training and competition, separately togglable), results-published notification, and a daily WP-Cron reminder that fires at 08:00 and emails everyone registered for a session in the next 23–25 hours. The reminder uses a PHP template at templates/email/reminder-training.php; each send — success or failure — writes an audit-log row so the board can see delivery at a glance.

Document library. Board documents (meeting minutes, assembly records, rulebooks, forms) are stored with three access tiers: public, members, and board. The document type taxonomy maps cleanly to how Slovenian sports clubs are legally required to maintain their records.

GDPR. Slovenia is EU. The plugin registers with WordPress’s native privacy tooling — a named exporter and a named eraser under Tools → Export / Erase Personal Data. The eraser deletes registrations but anonymizes result rows: user_id is set to NULL (schema-level nullable since DB version 1.1.0), scores and timestamps are preserved, so a member’s erasure request doesn’t punch holes in the club’s historical leaderboards.

Quality baseline. The codebase runs PHPStan at level 5 with the szepeviktor/phpstan-wordpress plugin, PHPCS against WordPress Coding Standards, and a hybrid PHPUnit suite — Brain Monkey for pure unit tests of Services and Models (no WordPress bootstrap required), wp-phpunit for integration tests of repositories, REST controllers, and shortcodes. Every CI push runs all three tools.

Why this matters

This is the kind of project that illustrates what “T4 Care” actually means in practice. The club has maybe sixty active members. Nobody would notice if the registration timestamps were second-precision instead of microsecond-precision. Nobody would notice if GDPR erasure just deleted the result rows. Nobody would notice if there were no audit log.

I built it with those details anyway — because the sixty members are real people, because Slovenian GDPR enforcement is real, and because the club secretary deserves infrastructure that won’t embarrass them in two years. The scale is a fraction of a fintech platform. The care isn’t.

03 What I delivered · challenges solved

Six things shipped,
three hard ones solved.

Key contributions

  • Designed and built the entire custom WordPress plugin from scratch — PSR-4 OOP architecture across Models, Repositories, Services, API, Admin, and Frontend layers.
  • Implemented a four-tier role and capability system (member, competition admin, board member, administrator) with 14 granular permissions.
  • Built training session management with recurring events, capacity caps, conflict detection, and microsecond-precision registration timestamps to resolve simultaneous signups fairly.
  • Built competition management with multi-day slot allocation, lane assignment, and a three-preference slot system so members influence their own schedule.
  • Implemented a season ranking engine: dense-rank algorithm per discipline, configurable best-of-N scoring rules, event-driven recalculation on every result save.
  • Built iCal feed generation (public + per-member token-authenticated personal feed) so members sync the club calendar directly to Google Calendar or Apple Calendar.
  • Wired a daily WP-Cron reminder pipeline that sends 08:00 email alerts for next-day sessions — with audit-log entries per send and graceful failure isolation.
  • Integrated WordPress's native GDPR exporter and eraser — registrations deleted on request, competition results anonymized rather than deleted so historical rankings stay intact.
  • Set up a full PHPUnit test suite (Brain Monkey for unit, wp-phpunit for integration) with PHPStan static analysis and PHPCS (WordPress Coding Standards) in CI.

Challenges solved

  • Simultaneous registration races — solved with microsecond-precision datetime(6) timestamps and a unique constraint on (session_id, user_id), giving a deterministic queue order without application-level locks.
  • GDPR vs. historical integrity tension — competition results needed to survive erasure requests without corrupting season rankings; solved by anonymizing the user_id column to NULL rather than deleting the score row.
  • Event-driven ranking recalculation — dropping the cron-based approach in favour of a synchronous sdr_result_saved action hook that rebuilds the ranking cache per (discipline, season) immediately on result insert or update.
A small club's backend deserves the same rigour as a fintech platform. Audit trails, GDPR erasure, race-safe registrations — the scale is different, but the club members are just as real.
Davor Majc, Lead Developer / SD Radovljica
04 Tech stack

What's under the hood.

PHPWordPressMySQLWP-CronPHPUnitBrain MonkeyPHPStanPHPCSiCalGDPR
Let's talk

¿Listo para arreglar, construir
o escalar?

30 minutos, conmigo personalmente. Leo tu sistema como un archivo de logs y te digo qué haría primero. Sin presentaciones, sin embudo de ventas.

Davor Majc, fundador, Numen

What you get on call
→ un diagnóstico de una página
→ 2–3 formas de solución, ordenadas por impacto
→ coste aproximado + plazo para cada una
→ sí/no — ¿soy la elección adecuada?
+386 40 828 474 · Blejska Dobrava, SI