Back
Full-Stack Engineering

Event-Crypt

Event-Crypt architecture diagram

Summary

(7 min read)

Event-Crypt is a multi-tenant platform for organization-scoped award voting and ticketing events. I built the Vue + Inertia web app, the Laravel backend (payments, USSD, admin), and the operational setup (CI/CD, Nginx + PHP-FPM, workers) to run it reliably in production.

Project Snapshot

My Role

Full-Stack Engineer (Web · Backend · DevOps)

Duration

2026

Context

Multi-tenant Product

Outcome

Web + USSD · layered payment verification · queue/scheduler reconciliation · CI/CD + Nginx/PHP-FPM deployment

Stack

Laravel 12PHP 8.2+Vue 3TypeScriptInertiaViteFilamentPostgreSQLLaravel QueueLaravel SchedulerNginxPHP-FPMLinuxHubtel PaymentsPaystack (legacy)CloudinaryCI/CD (GitHub Actions)

The Problem

Context

Organizations running awards and ticketed events need a single platform where each organization can create events, collect votes and ticket purchases, and reliably reconcile payments across multiple user channels.

The Pain

Payments can be inconsistent when webhooks are delayed, duplicated, or missed. USSD sessions require a deterministic state machine, and multi-tenant boundaries must be enforced everywhere (routes, queries, admin).

Why It Mattered

Incorrect fulfillment (missing votes, duplicate votes, unfulfilled tickets) breaks trust and directly impacts revenue. The system must be auditable and resilient under provider variability.

Goals & Requirements

Technical Goals

  • Multi-tenant boundary model (Organization → Events)
  • Web voting/ticketing flow (Vue + Inertia) with reliable payment fulfillment
  • USSD voting channel with cache-backed session state
  • Webhook handling with idempotency guards
  • Fallback payment verification (success page + queued jobs)
  • Admin operations via Filament (org + super-admin panels)
  • CI/CD pipeline and repeatable deployments (Nginx + PHP-FPM + workers)
  • Notifications via SMS + receipts

Constraints

  • Handle duplicate/retried provider callbacks safely
  • Ensure eventual consistency even when webhooks are missed
  • Keep tenant scoping explicit and enforceable

Architecture Design

Clients (web, admin, USSD gateway) talk to a Laravel monolith with domain services for voting, ticketing, payments, and notifications. Payments integrate primarily with Hubtel, with layered verification (webhook + success verification + scheduled reconciliation). Data is stored in PostgreSQL, and async workloads run via queue jobs and the Laravel scheduler.

Architecture Diagram

Scroll horizontally on smaller screens to view full diagram

Component Breakdown

Web App (Vue 3 + Inertia)

Voting and ticketing UI for end users.

USSD Gateway Integration

USSD voting channel via /api/ussd and a session state machine.

Laravel Monolith

Routes/middleware, controllers, domain services, and queued jobs for voting/ticketing/payments.

Nginx + PHP-FPM

Production runtime: serves the web app and API, and runs queue workers and scheduled jobs alongside the app.

Filament Admin Panels

Organization-scoped operations: event setup, nominee management, verification, monitoring.

PostgreSQL

Primary datastore for tenants, events, votes, tickets, and payments.

Queue + Scheduler

Runs verification jobs and periodic reconciliation for pending payments.

Hubtel + Paystack

Payment initialization, status checks, webhooks, and (legacy) parallel payment support.

SMS Providers

Delivers receipts/notifications for successful votes and ticket purchases.

Cloudinary

Media storage and delivery for event assets.

Key Design Decisions

Layered payment verification

Combining webhook processing with success-page verification and scheduled reconciliation reduces lost-payment risk and improves eventual consistency.

Cache-backed USSD state machine

USSD is conversational and stateless per request; caching session state makes flows deterministic and resilient.

Tenant scoping in routes + queries

Multi-tenancy must be enforced consistently to prevent data leakage across organizations.

Implementation Breakdown

01

Component architecture

The platform is organized as a Laravel monolith with domain services, async jobs, and clear boundaries to external providers.

  • Multiple client channels: web, admin, and USSD
  • Domain services for payments, voting, receipts, and notifications
  • Queue jobs + scheduler for retries and reconciliation
High-level architecturemermaid
Rendering diagram…
View diagram source
flowchart LR
    subgraph Clients
      WEB["Web Browser<br/>Voting/Ticketing"]
      USSDGW["USSD Gateway/Telco"]
      ADMIN["Admin/Super Admin<br/>Filament UI"]
    end

    subgraph App["Laravel Monolith"]
      ROUTES["Routes + Middleware"]
      CTRL["Controllers"]
      SVC["Domain Services<br/>Hubtel, Voting, Receipt, Notifications"]
      JOBS["Queue Jobs + Scheduler"]
      MODELS["Eloquent Models + Scopes"]
      CACHE["Cache Store"]
    end

    subgraph Data
      DB["PostgreSQL / SQLite"]
    end

    subgraph External
      HUBTEL["Hubtel APIs<br/>Checkout / Status / Send Money"]
      PAYSTACK["Paystack APIs<br/>Legacy/parallel support"]
      SMS["SMS Providers<br/>Hubtel/Arkesel"]
      CLOUD["Cloudinary"]
      CI["GitHub Actions CI"]
    end

    WEB --> ROUTES
    ADMIN --> ROUTES
    USSDGW --> ROUTES
    ROUTES --> CTRL --> SVC --> MODELS --> DB
    CTRL --> CACHE
    SVC --> CACHE
    SVC --> HUBTEL
    SVC --> PAYSTACK
    SVC --> SMS
    SVC --> CLOUD
    JOBS --> SVC
    JOBS --> DB
    JOBS --> HUBTEL
    CI --> App
02

Payment reliability (webhook + verification + reconciliation)

Payments are fulfilled via a layered approach to minimize missed callbacks and handle provider variability.

  • Primary: Hubtel webhook endpoint
  • Secondary: success-page verification using provider status endpoint
  • Background: queued verification jobs + scheduled sweeps for stale pending payments
Web voting flowmermaid
Rendering diagram…
View diagram source
sequenceDiagram
    participant U as Voter (Web)
    participant FE as Vue + Inertia
    participant BE as Laravel Controllers/Services
    participant HP as Hubtel Checkout
    participant WH as Hubtel Webhook
    participant JOB as Verification Jobs
    participant DB as Database

    U->>FE: Select nominee/category + quantity
    FE->>BE: Submit vote checkout request
    BE->>DB: Create pending payment
    BE->>HP: Initialize checkout
    HP-->>U: Checkout URL / redirect
    U->>HP: Complete payment
    HP->>WH: Payment callback
    WH->>BE: Process webhook payload
    BE->>DB: Mark payment successful
    BE->>DB: Create votes + increment nominee totals
    Note over JOB,BE: Fallback verification runs if webhook delayed/missed
03

USSD voting state machine

USSD requests are processed through a cache-backed state machine keyed by session ID.

  • Cache key per session (ussd_state_<sessionId>) with TTL
  • Deterministic level-by-level prompts and transitions
  • Charges initiated via provider service once user confirms
USSD flowmermaid
Rendering diagram…
View diagram source
sequenceDiagram
    participant T as Telco/USSD Gateway
    participant UC as UssdController
    participant C as Cache
    participant HS as HubtelService
    participant DB as Database

    T->>UC: /api/ussd (new session)
    UC->>C: Create ussd_state_<sessionID>
    T->>UC: User code/category/quantity/confirm inputs
    UC->>C: Update session state per level
    UC->>DB: Create pending payment
    UC->>HS: Charge mobile money
    HS-->>UC: Prompt/PIN/OTP status
    UC-->>T: Continue/end session response
04

DevOps: CI/CD and production runtime

I handled the deployment and operational setup so the platform could run reliably in production.

  • GitHub Actions CI to run checks and build the web assets
  • Repeatable server deployment steps (Nginx + PHP-FPM + queues + scheduler)
  • Operational jobs for payment reconciliation and status verification

Challenges & Solutions

#1Idempotency under provider retries

The Problem

Payment providers may retry webhooks and users may refresh/return to success pages, causing duplicate fulfillment risk.

The Fix

Implemented idempotency guards around payment fulfillment (status checks and usage checks) so votes/tickets are created exactly once per successful transaction.

#2Tenant boundary enforcement

The Problem

Multi-tenant systems can accidentally leak data if organization scoping is inconsistent.

The Fix

Scoped public routes by organization slug and enforced ownership checks and scoped queries throughout controllers and admin panels.

Results & Impact

A resilient multi-tenant platform that supports both web and USSD channels with reliable payment fulfillment and admin operational tooling.

Before → After

Payment completion paths

Webhook-onlyWebhook + verification + reconciliation
Higher reliability

Admin operations

ManualFilament panels + scoped dashboards
Faster workflows

Channel support

Web onlyWeb + USSD
Wider reach

Business Outcome

Organizations can run multiple award and ticketing events in one platform, reconcile payments reliably, and operate events with admin verification tools and notifications.

Reflections

Would Do Differently

  • 01Add structured observability (metrics + alerts) early, especially around webhook latency and reconciliation rates
  • 02Introduce stronger idempotency keys and unique constraints for provider transaction references

Key Takeaways

  • 01For payments, design for retries and missed callbacks from day one
  • 02USSD flows require explicit state handling — caching is the simplest reliable foundation

Next Project

Private Learn

Backend Engineering

Private Learn

Thanks for Reading