Skip to content

kazemmdev/events-app-php

Repository files navigation

Events — micro events service

An admin creates Events (content, thumbnail, details, tags, category, expiry time). Users subscribe to events. When the admin posts a Story (update) inside an event, every subscriber is notified automatically across three channels:

  • In-app, real-time — Redis pub/sub → gateway SSE → browser notification bell
  • Email — SMTP (Mailpit in dev, SES-compatible in prod)
  • Web push — VAPID browser push

Architecture

Next.js web ──REST──▶ api-gateway ──gRPC──▶ user-service         (users schema)
                          │        ──gRPC──▶ event-service        (events schema: subscriptions + outbox)
                          │        ──gRPC──▶ notification-service (notifications schema)
                          └──SSE (Redis pub/sub) ◀── notification-service

event-service: story write + outbox row in ONE transaction
   └▶ outbox-publisher ─▶ RabbitMQ (topic `events`, key `story.created`)
        └▶ fanout-worker ─▶ `notification.requested` per subscriber
             └▶ notification consumer ─▶ persist in-app notif + dispatch:
                   • in-app  → Redis pub/sub → gateway SSE → browser
                   • email   → SMTP (Mailpit dev / SES prod)
                   • webpush → VAPID

Reliability properties:

  • Transactional outbox — a story and its story.created outbox row commit atomically; the outbox-publisher worker delivers at-least-once to RabbitMQ.
  • Idempotent consumers — every notification.requested message carries a deterministic id (story-{id}-user-{id}); the notification consumer dedupes on it, so redeliveries never create duplicate notifications.
  • Dead-letter queues — failing messages land in <queue>.dlq instead of poisoning the queue.
  • Expiry scheduler — a k8s CronJob marks events past expired_time as expired (hidden from public listings) and emits event.expired.

Repository layout

Path What it is
services/api-gateway REST gateway (:9000): auth, events, uploads, notifications, SSE
services/user-service gRPC (:50051): users, bcrypt credentials, roles
services/event-service gRPC (:50052): events/categories/tags/stories/subscriptions/outbox + workers
services/notification-service gRPC (:50053): notifications + push subscriptions + consumer
shared/ Router, HTTP envelope, Validator, DB, RabbitMQ/Redis buses, generated proto stubs
protos/ user.proto, event.proto, notification.proto
migrations/ 0000_create_schemas.sql + one directory per service (schema-per-service)
infra/dev Dockerfiles + k8s manifests (Tilt-driven)
web/ Next.js frontend (browse/subscribe, notification bell, admin dashboard)

Prerequisites

PHP 8.4+, Composer, protoc + grpc_php_plugin (for proto regeneration), Docker + a local k8s cluster + Tilt, Node 22+ for the frontend. The grpc/protobuf PHP extensions are only needed inside the containers — the shared base image (infra/dev/docker/php-base.dockerfile) builds them.

Getting started

cp .env.example .env.local        # adjust if needed
source .env.local

tilt up                           # mysql, rabbitmq, redis, minio, mailpit + all services & workers
make db-migrate                   # creates schemas (users/events/notifications) + tables

cd web && npm install && cp .env.example .env.local && npm run dev

Dev endpoints once Tilt is green:

URL Service
http://localhost:9000 API gateway
http://localhost:3000 Next.js frontend
http://localhost:8025 Mailpit inbox
http://localhost:15672 RabbitMQ management (guest/guest)
http://localhost:9901 MinIO console

Creating an admin

Register through the UI (or POST /api/auth/register), then promote:

mysql -h 127.0.0.1 -u root -p'root_password_123!' \
  -e "UPDATE users.users SET role='admin' WHERE email='you@example.com'"

Log out and back in so the JWT picks up the new role.

Enabling web push (optional)

npx web-push generate-vapid-keys

Put the keys in infra/dev/k8s/app-secret.yaml (VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY) and in web/.env.local (NEXT_PUBLIC_VAPID_PUBLIC_KEY), then restart Tilt.

API overview

Public: POST /api/auth/register|login, GET /api/auth/me, GET /api/events[/{id}[/stories]], GET /api/categories, GET /api/tags.

Authenticated: POST|DELETE /api/events/{id}/subscribe, GET /api/me/subscriptions, GET /api/me/notifications, POST /api/me/notifications/{id}/read, POST /api/me/notifications/read-all, POST /api/me/push-subscriptions, GET /api/notifications/stream (SSE; accepts ?token=).

Admin: GET|POST /api/admin/events, GET|PUT|DELETE /api/admin/events/{id}, POST /api/admin/events/{id}/stories, POST|DELETE /api/admin/categories[/{id}], POST|DELETE /api/admin/tags[/{id}], POST /api/admin/uploads (presigned MinIO/S3 upload).

Tests

Each service has a Pest suite (services/<name>/tests). DB-backed tests provision isolated <schema>_test databases from the migration files and skip automatically when MySQL is unreachable.

make test                                     # all suites
cd services/event-service && ./vendor/bin/pest   # one suite

CI (.github/workflows/ci.yml) runs every suite against a MySQL service container plus a frontend lint + build.

Regenerating gRPC stubs

make generate-proto    # emits PHP classes into shared/Proto/*

Verifying the core flow end-to-end

  1. Register two users; promote one to admin (see above).
  2. As admin: create a category/tags, create a published event with a thumbnail.
  3. As the second user: subscribe to the event; keep the site open (bell connects via SSE).
  4. As admin: post a story on the event.
  5. Observe: outbox row → RabbitMQ → notification row, the bell updates live, the email appears in Mailpit (http://localhost:8025), and web push fires if configured.
  6. Redeliver the same message (RabbitMQ management → move from queue) → no duplicate notification.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages