This project has been created as part of the 42 curriculum by yandry, tstephan, jramiro, achu, sliziard.
A real-time collaborative messaging platform built as the final project of the 42 Common Core. Inspired by Discord, but much better <3, it features servers (guilds), channels, direct messages, real-time chat, a friend system, a full role/permission system and of course voice / video calls! All backed by an event-driven microservices architecture because we love to suffer :)
- Description
- Instructions
- Resources
- Team Information
- Project Management
- Technical Stack
- Database Schema
- Features List
- Modules
- Individual Contributions
ft_discord is a full-stack web application that replicates the core experience of a real-time messaging platform. Users can register, create and join servers (guilds), communicate in text channels, send direct messages, manage friends, call their friends on voice or video and interact with a rich notification system.
- Real-time messaging via WebSockets (SignalR)
- Server (guild) creation with channels, categories and role-based permissions
- Friend system with online presence indicators
- Direct messaging and a real-time notification system
- 1-on-1 voice and video calls via WebRTC
- Email + password authentication and OAuth 2.0 (Google, GitHub, 42)
- Event-driven backend with a message queue (RabbitMQ)
- Polyglot microservices architecture, each service independently deployable
- Monitoring stack: Prometheus + Grafana dashboards fed by per-service OpenTelemetry exporters
- Containerized and launchable with a single command
- Docker and Docker Compose, or Podman +
podman-compose(themakeflow autodetects the engine, preferringpodman/podman-composeand falling back todocker/docker compose; force one withENGINE=podman makeor, if you're normal and prefer Docker,ENGINE=docker make) - GNU Make (the entry point for every command)
- Go (only needed once for
tools/secretmanbootstrap) - Tilt (only needed for the local development loop, not for the eval
makeflow) - An
agekey registered intools/secretman/.sops.yaml(one of the maintainers can add yours)
The full setup is driven by Makefile targets at the repo root. A normal first-run looks like:
# 1. Clone the repository
git clone git@github.com:vantavoids/ft_transcendence.git ft_discord
cd ft_discord
# 2. Bootstrap secretman (downloads SOPS + age binaries on first run, registers your age key)
make secrets-setup
# 3. Decrypt every per-service .env file in place
make secrets-decrypt
# 4. Build and start the full stack (runs check-env, then build, then up -d)
makeThe application will be available at https://localhost:1443. NGINX terminates TLS on 1443; 1080 redirects HTTP to HTTPS.
| Target | Description |
|---|---|
make (alias for make all) |
check-env • build • up -d • the canonical "bring the project up" command |
make build |
Build all images |
make up |
Start the stack in detached mode |
make down |
Stop the stack |
make re |
fclean + all (scorched-earth rebuild) |
make clean |
down + prune dangling images |
make fclean |
Nuke containers, images and volumes (use with care) |
make rmcert |
Remove the generated mkcert certificates |
make logs |
Tail every container's logs (Ctrl+C to detach) |
make ps |
List running containers |
make login |
docker login / podman login against docker.io for image pulls |
make dev |
Bring the stack up under Tilt with file-watching live reload |
make secrets-setup |
Bootstrap tools/secretman and register your age key |
make secrets-decrypt |
Decrypt every per-service .env.* file in place |
make secrets-encrypt |
Re-encrypt every per-service .env.* file |
make secrets-refresh |
Re-encrypt with the current .sops.yaml recipient list (useful after a new contributor's key is added) |
make check-env |
Verify all expected .env files exist and have at least one populated variable |
make check-env runs before any build / start target and verifies that every .env file exists and has at least one populated variable. The expected files are:
.env
frontend/.env
services/auth/.env
services/chat/.env
services/gateway/.env
services/guild/.env
services/notification/.env
services/user/.env
The root .env only carries values shared across every service (copy from .env.example):
| Variable | Description |
|---|---|
BASE_URL |
Public root URL of the stack (e.g. https://localhost:1443) |
BASE_API_URL |
Public API root (e.g. https://localhost:1443/api) |
JWT_SECRET |
Shared HMAC key used by Auth (sign) and the Gateway (verify) |
RABBITMQ_USER |
RabbitMQ default user |
RABBITMQ_PASS |
RabbitMQ default password |
OAuth client IDs / secrets, database passwords, the Grafana admin password, TURN credentials and other per-service secrets live in each service's own SOPS-encrypted env file under services/<name>/ and frontend/, and are decrypted in place by make secrets-decrypt.
make downmake dev brings the stack up under Tilt, so each service hot-reloads on file change via its own Tiltfile. Useful when you are iterating on a single service and do not want full docker compose up --build cycles.
- Next.js documentation
- React documentation
- Tailwind CSS documentation
- Zod documentation
- ASP.NET Core documentation
- SignalR documentation
- Entity Framework Core documentation
- Carter (HTTP modules for ASP.NET)
- MassTransit documentation
- NestJS documentation
- node-postgres (
pg) documentation - RabbitMQ documentation
- ScyllaDB documentation
- OpenTelemetry documentation
- Prometheus documentation
- Grafana documentation
- WebRTC API documentation
- coturn STUN/TURN server
- OAuth 2.0 RFC 6749
- Tilt documentation
- SOPS documentation
- WCAG 2.1 quick reference
Ahh, our beloved sixth member. AI was used throughout the project as a productivity and learning aid. We treated it as a fast pair-programmer and a searchable second opinion, never as an authority: every suggestion was read, understood and adapted before it entered the codebase, and the person who owns a service is accountable for all of its code regardless of how any individual line was drafted.
Concretely, AI was used for:
- Debugging and rubber-ducking. Explaining stack traces, narrowing down concurrency and cross-service bugs, and sanity-checking our reasoning on tricky flows (transactional outbox, permission resolution, SignalR connection tracking, ICE negotiation).
- Code review support. A first-pass reviewer for diffs, in addition to the mandatory human review every PR still went through before the merge.
Where we drew the line: all architectural decisions (the microservice split, service ownership, communication contracts, storage choices) were made by the team, not delegated to a model. No secrets or private keys were ever shared with an AI tool. AI output was never merged unread, and anything we could not explain ourselves did not ship. In the spirit of the 42 curriculum, the goal was to learn faster, not to skip the learning.
| Login | Role(s) | Responsibilities |
|---|---|---|
yandry |
Product Owner, Developer | Product backlog, Guild Service (incl. roles & permissions), Chat Service (primary, real-time messaging via SignalR + ScyllaDB + WebRTC signaling), infrastructure & devops (Docker Compose, Tilt, NGINX, RabbitMQ, Prometheus/Grafana monitoring) |
tstephan |
Tech Lead, Developer | Technical architecture decisions, User Service (profiles, avatars, friends, presence), Frontend (shared with jramiro) |
sliziard |
Project Manager, Developer | Sprint planning, Auth Service (register/login/OAuth/JWT/refresh), Chat Service (co-developer) |
jramiro |
Developer | API Gateway, secret management tooling (tools/secretman), Frontend (shared with tstephan), monitoring (Prometheus/Grafana, shared with yandry), Privacy Policy & ToS pages |
achu |
Developer, UX | Notification Service (incl. mailer), Chat Service (co-developer) |
Each backend service has a single designated owner who oversees its design, reviews contributions to it and is the person evaluators should question first about that service. Other teammates are welcome to send PRs against any service, but the owner holds the architectural line.
This mirrors how real companies running microservices actually operate. At places like Amazon, Spotify, Netflix or any modern shop where a product is split into independently-deployable services, each service is owned by a small team (Amazon famously caps them at "two pizzas") that is responsible end-to-end for its design, code, deployment, monitoring and on-call. Outside contributors can send PRs into another team's service, but the owning team is the architectural authority and the first reviewer; the broader org trusts the owning team to keep that service coherent rather than designing it by committee. We are scaled down to one person per service instead of a team per service, but the pattern is the same: one place to make architectural calls, one person whose name is on it during evaluation, no diffuse responsibility. This keeps decisions coherent across a polyglot codebase without freezing anyone out of any area, and it matches the way the team would scale if the project kept growing past five people.
| Service | Owner |
|---|---|
| Auth Service | sliziard |
| API Gateway | jramiro |
| User Service | tstephan |
| Guild Service | yandry |
| Notification Service | achu |
| Chat Service | yandry (designated; see note) |
Chat is the exception. It has been a collective effort with sliziard contributing actively alongside yandry, rather than fitting cleanly under a single owner. We still designate yandry as the owner of record so that there is one person to ask about Chat-Service architecture at evaluation time, but credit on Chat is shared across all three.
One-page architecture: services own their data, RabbitMQ spreads gossip, SignalR screams live updates, WebRTC negotiates who gets to hear you, and NGINX slaps TLS on all of it so it looks professional.
Work was broken down into services aligned with team ownership. Each person owned one or more backend services end-to-end (design to implementation to tests to containerization to CI integration).
- GitHub Projects ("The Holy Board"): backlog grooming, sprint planning, issue prioritization
- GitHub Issues: every feature, bug and chore tracked as an issue and pulled into a sprint iteration
- GitHub Pull Requests: every change reviewed by at least one other team member before merge, with linked issues
- Discord: daily async communication and weekly sync meetings
- Weekly sync: progress review, blockers, next sprint planning
- Async standups on Discord: daily updates in a dedicated channel
| Technology | Purpose |
|---|---|
| Next.js 16 (App Router) | React framework, routing, server components |
| React 19 + TypeScript 5.7 | UI components and type safety |
| Tailwind CSS 3.4 | Styling and design system |
| Zod | Runtime input validation (mirrors backend validation) |
| lucide-react | Icon set |
| pnpm | Package manager |
| ESLint + Prettier + Husky + lint-staged | Linting / formatting, enforced via pre-commit hook |
The polyglot architecture lets each service pick the best tool for the job. Services communicate only via REST (sync, when an answer is required before continuing) and RabbitMQ events (async, fire-and-forget).
| Service | Language / Framework | Purpose |
|---|---|---|
| API Gateway | Go 1.25 (golang-jwt) |
Reverse proxy for /api/{service}/vN/..., local JWT validation, rate limiting |
| Auth Service | ASP.NET Core 10 (C#), Clean Architecture | Registration, login, OAuth 2.0 (Google / GitHub / 42), JWT issuance & refresh, account deletion |
| User Service | NestJS (TypeScript, Node.js; pg) |
Profiles, avatars, banners, friends, blocks, online status |
| Guild Service | ASP.NET Core 10 (C#), Clean Architecture, EF Core | Guilds, channels, categories, membership, roles, invites, bans, permission overwrites |
| Chat Service | ASP.NET Core 10 (C#), Clean Architecture, SignalR | Channel messages, DMs, attachments, reactions, read state, WebRTC signaling |
| Notification Service | Go 1.25 | Consumes events, persists notifications, serves read endpoints, pushes real-time updates via SSE |
| Storage | Used by | Notes |
|---|---|---|
| PostgreSQL | Auth, User, Guild, Notification | One database per service (no cross-service DB access) |
| ScyllaDB (Cassandra-compatible) | Chat | Wide-column store, partitioned by channel / conversation |
| Technology | Purpose |
|---|---|
| RabbitMQ | Message queue for inter-service events (MassTransit on the .NET side) |
| NGINX | Reverse proxy + TLS termination (1443 HTTPS, 1080 HTTP to HTTPS redirect) |
| Docker + Docker Compose | Containerization, single-command deploy via root compose.yaml (composes per-service files) |
| Tilt | Local dev with file-watching live reload |
SOPS + age (tools/secretman) |
Encrypted per-service env files committed to Git |
| OpenTelemetry | Per-service SDK exposing a Prometheus /metrics endpoint (traces wired for forward-compatibility) |
| Prometheus | Metrics collection from every service |
| Grafana | Metrics dashboards |
| coturn | Self-hosted STUN/TURN server for WebRTC NAT traversal |
| MailHog | SMTP capture for confirmation emails (account deletion, data export ready); swapped for a real SMTP provider in prod |
| MinIO | Object storage for avatars, banners, chat attachments and data export |
- Polyglot microservices. Each service owns its stack. The communication contract (REST + RabbitMQ) is the only thing services need to agree on, so the internal tech of each service is an implementation detail and each owner picks what they're most productive in.
- Next.js (frontend). Built-in App Router, server components, image optimisation and a TypeScript-first DX. We use it as a frontend-only framework and pair it with separate backend services, which still qualifies as a Major per v21 III.3.
- ASP.NET Core + Clean Architecture (Auth, Guild, Chat). Strong typing, first-class SignalR for WebSockets, EF Core for Guild's relational model, and a layered structure (Domain / Application / Infrastructure / Persistence / Presentation) that keeps business rules off the framework. Carter handles minimal HTTP module routing on top of the standard ASP.NET pipeline.
- NestJS +
pg(User). The User Service is read-heavy and I/O-bound (friend lists, presence fan-out), which Node's event loop handles well. NestJS gives a layered module/DI structure with class-validator DTOs, and thepgdriver keeps SQL explicit (no ORM). Keeping it in TypeScript also shares one language with the frontend. (Originally prototyped in Rust/axum; migrated to NestJS for stack consistency and developer velocity.) - Go (Gateway, Notification). Both are small, latency-sensitive, mostly I/O-bound services. Go's HTTP stack and goroutines are an excellent fit, and the resulting binaries are tiny.
- RabbitMQ + MassTransit. Lightweight, easy to self-host, supports both pub/sub and work queues. MassTransit on the .NET side lets us treat queues as strongly typed message buses with automatic exchange / queue setup and contract-based routing.
- PostgreSQL. Default choice for relational data (Auth, User, Guild, Notification). One database per service enforces ownership boundaries.
- ScyllaDB (Chat). Chat is write-heavy and the natural access pattern is time-ordered by channel or conversation. A partitioned wide-column store handles this far more efficiently than a relational DB at scale; CQL compatibility lets us keep the schema versioned in
docs/schema/chat.cql. - NGINX. TLS termination only, plus an HTTP to HTTPS redirect. All authorisation and routing happens behind it in the Gateway.
- SOPS + age (
tools/secretman). Encrypted env files live in the repo so contributors can decrypt their own copy without an out-of-band secret share. Per-service files keep blast radius small. - Tilt.
tilt upbrings every service up with watch-mode rebuild, which is much faster than fulldocker compose up --buildcycles during development. - OpenTelemetry + Prometheus + Grafana. Each service is instrumented with the OpenTelemetry SDK and exposes a Prometheus
/metricsendpoint via the SDK's Prometheus exporter; Prometheus scrapes it and Grafana visualises. Using OTel up-front keeps the door open for adding traces (and centralised logs) later without re-instrumenting every service.
Full DDL lives in docs/schema/. Summary below.
| Service | Storage | Files |
|---|---|---|
| Auth | PostgreSQL | docs/schema/auth.sql |
| User | PostgreSQL | docs/schema/user.sql |
| Guild | PostgreSQL | docs/schema/guild.sql |
| Chat | ScyllaDB (CQL) | docs/schema/chat.cql |
| Notification | PostgreSQL | docs/schema/notification.sql |
| Table | Service | Notes |
|---|---|---|
users_auth |
Auth | credentials, refresh token, soft-delete |
users_profile |
User | display info, avatar, banner, last-seen |
friendships |
User | pending / accepted state machine, canonical ordering constraint |
user_blocks |
User | unilateral blocks (independent of friendship) |
guilds |
Guild | name, icon, owner |
channel_categories |
Guild | collapsible channel groups |
channels |
Guild | text / announcement, NSFW flag, slowmode |
guild_members |
Guild | membership + per-guild nickname |
guild_bans |
Guild | separate from membership; supports pre-emptive bans |
guild_invites |
Guild | invite codes with expiry, use cap, revoke flag |
roles |
Guild | bitmask, position, hoisted, mentionable |
member_roles |
Guild | user to role per guild; composite FK prevents cross-guild assignment |
channel_permission_overwrites |
Guild | per-channel allow/deny by role or user |
messages |
Chat (ScyllaDB) | partitioned by channel_id, soft-delete, reply threading |
direct_messages |
Chat (ScyllaDB) | partitioned by conversation_id |
message_attachments / dm_attachments |
Chat (ScyllaDB) | file metadata per message |
message_reactions / message_reaction_counts |
Chat (ScyllaDB) | reactions + COUNTER totals |
channel_read_states |
Chat (ScyllaDB) | last-read position per user per channel |
user_conversations |
Chat (ScyllaDB) | DM thread index; dm_unread_counts for unread badges |
message_lookup |
Chat (ScyllaDB) | message_id to full PK + author_id; routes channel vs DM mutations |
notifications |
Notification | actor, source, expires, dismiss tracking |
notification_preferences |
Notification | per-user mute by guild or channel |
| Feature | Description | Implemented by |
|---|---|---|
| User registration | Email + password signup with hashed/salted passwords | sliziard |
| User login | JWT-based session, secure cookie handling | sliziard |
| OAuth 2.0 | Login with Google, GitHub or 42 | sliziard |
| User profiles | Avatar upload, display name, bio, status | tstephan |
| Friend system | Send/accept/decline friend requests, view friends list | tstephan |
| Online presence | Real-time online / idle / offline indicators | tstephan (User Service), yandry (Chat Service hub) |
| Guild creation | Create, edit, delete servers with icon and description | yandry |
| Channels | Create text channels and categories within a guild, manage order | yandry |
| Guild membership | Invite users, leave guilds, kick members | yandry |
| Role system | Create roles with color, position and permission bitmask | yandry |
| Permissions | Per-role access control with channel-level overwrites | yandry |
| Real-time chat | WebSocket-based messaging in channels and DMs via SignalR | yandry, sliziard |
| Message history | Persistent chat history, paginated load | yandry, sliziard |
| Direct messages | Private 1-on-1 real-time messaging | yandry, sliziard |
| File uploads | Avatars, banners, chat attachments (multipart, type / size validation, MinIO storage) | tstephan (avatar/banner + frontend), yandry (chat attachments + MinIO container) |
| Notification system | Mentions, DM alerts, friend requests, real-time + persistent | achu |
| API Gateway | Centralized routing, JWT validation, rate limiting | jramiro |
| Message queue | RabbitMQ event bus between services | yandry |
| Monitoring | Prometheus metrics + Grafana dashboards for every service, fed by per-service OpenTelemetry exporters | yandry, jramiro |
| Containerization | Full Docker Compose setup, single-command launch | yandry |
| HTTPS | NGINX reverse proxy with TLS | yandry |
| Secret management | SOPS-encrypted per-service env files via tools/secretman |
jramiro |
| Frontend | Next.js 16 App Router, routing, real-time updates | tstephan, jramiro |
| Multi-browser support | Tested and supported on Chrome (mandated), Firefox and Safari with broad feature parity | tstephan, jramiro |
| Server-Side Rendering | App Router renders Server Components by default for fast first paint and SEO | tstephan, jramiro |
| Design system | Tailwind tokens (palette, typography, spacing, shadows) + 11 reusable components, lucide-react icons | tstephan, jramiro |
| Voice calls | 1-on-1 real-time voice via WebRTC P2P | yandry (signaling, frontend client + UI, coturn infra) |
| Video calls | 1-on-1 real-time video via WebRTC P2P | yandry (signaling, frontend client + UI, coturn infra) |
| Privacy Policy | Accessible from footer, project-relevant content | jramiro |
| Terms of Service | Accessible from footer, project-relevant content | jramiro |
| GDPR: data export | "Download my data" button aggregates every service's data on the user into a single JSON bundle | all (cross-service) |
| GDPR: account deletion | DELETE /auth/me with confirmation flow + cross-service cascade (Auth, User, Guild, Chat, Notification consume user.deleted) + confirmation email |
all (cross-service) |
Modules implemented: 28 points - 14 baseline (2 Minor + 6 Major) + 14 bonus. Per chapter VII, at most 5 bonus points count toward the final grade, for a graded maximum of 19 (14 + 5).
Note: per chapter IV, bonus points are only counted after the baseline 14 are fully validated.
| # | Module | Category | Type | Points | Implemented by |
|---|---|---|---|---|---|
| 1 | Frameworks for frontend and backend (Next.js + ASP.NET Core + NestJS) | Web | Major | 2 | tstephan, jramiro (Next.js frontend), yandry, sliziard (ASP.NET backends), tstephan (NestJS backend) |
| 2 | Real-time features (WebSockets via SignalR) | Web | Major | 2 | yandry, sliziard |
| 3 | User Interaction (chat, profiles, friends, presence) | Web | Major | 2 | tstephan (profiles + friends), yandry, sliziard (chat) |
| 4 | Backend as microservices | Devops | Major | 2 | yandry (architecture + infra), jramiro (API Gateway) |
| 5 | Standard user management (profiles, avatars, friends, online status) | User Management | Major | 2 | tstephan |
| 6 | Organization system (guilds with channels and membership) | User Management | Major | 2 | yandry |
| 7 | OAuth 2.0 remote authentication | User Management | Minor | 1 | sliziard |
| 8 | Notification system (mentions, DMs, friend requests) | Web | Minor | 1 | achu |
| # | Module | Category | Type | Points | Implemented by |
|---|---|---|---|---|---|
| 9 | Monitoring with Prometheus + Grafana | Devops | Major | 2 | yandry, jramiro |
| 10 | ORM (Entity Framework Core, Guild Service) | Web | Minor | 1 | yandry |
| 11 | Advanced permissions system (roles, hierarchy, overwrites) | User Management | Major | 2 | yandry |
| 12 | 1-on-1 voice and video calls (WebRTC) | Module of choice (IV.10) | Major | 2 | yandry (signaling + coturn infra + frontend client + UI) |
| 13 | Custom design system (Tailwind tokens + 10+ reusable components) | Web | Minor | 1 | tstephan, jramiro |
| 14 | Server-Side Rendering (Next.js App Router) | Web | Minor | 1 | tstephan, jramiro |
| 15 | File upload and management (avatars, banners, chat attachments) | Web | Minor | 1 | tstephan (avatar/banner + frontend), yandry (chat attachments + MinIO container) |
| 16 | Multi-browser support (Firefox + Safari beyond mandated Chrome) | Accessibility | Minor | 1 | tstephan, jramiro |
| 17 | tools/secretman - SOPS-based encrypted env file workflow |
Module of choice (IV.10) | Minor | 1 | jramiro |
| 18 | GDPR compliance (data export, deletion with confirmation, confirmation emails) | Data and Analytics | Minor | 1 | all (cross-service) |
| 19 | Advanced chat features (block-to-message, typing indicators, read receipts, profile access from chat, history persistence) | Gaming and user experience (IV.6) | Minor | 1 | yandry, sliziard (chat), tstephan (block system + profile-from-chat) |
Module 1 - Frameworks for frontend and backend:
The frontend is a full Next.js 16 application (App Router, server components, TypeScript). The backend services use multiple frameworks: ASP.NET Core 10 with Clean Architecture for the C# services (Auth, Guild, Chat) and NestJS (TypeScript) for the User Service. v21 III.3 explicitly accepts Next.js for this module even when paired with separate backends, which is our case. The Go services (Gateway, Notification) use Go's standard net/http library rather than a third-party framework; they exist to support the overall architecture but do not contribute to this module's framework claim.
Module 2 - Real-time features (WebSockets):
Real-time messaging, presence updates, typing indicators, and WebRTC call signaling are all delivered over SignalR (an abstraction over WebSockets that gracefully degrades to long-polling when needed). The Chat Service owns the hub and pushes events to per-channel and per-user SignalR groups based on membership changes published by the Guild Service.
Module 3 - User Interaction:
The application combines a chat experience, user profiles, and a friend system. Users can browse profiles, send and respond to friend requests, see online presence, exchange messages and reactions in shared guild channels, and have private 1-on-1 DMs. These features are exercised together rather than in isolation.
Module 4 - Backend as microservices:
Each service (Auth, User, Guild, Chat, Notification) has a single responsibility, exposes a versioned REST API and communicates asynchronously via RabbitMQ for cross-service events (e.g. user.registered triggers the User Service to create a profile and the Notification Service to send a welcome; chat.message_sent fans out to all connected clients via SignalR). Services are loosely coupled and independently deployable via Docker. Synchronous internal HTTP calls are reserved for the cases where a service needs an authoritative answer before it can act. These include Chat asking Guild whether the sender is a channel member with the right permission before accepting a message, Guild resolving a user's existence and profile against the User Service, Notification checking the block relationship between two users before creating a mention or DM notification, Auth querying Guild's owned-guilds-count before allowing an account deletion, and the User Service fanning out to every other service's /internal/.../data-export endpoint to assemble a GDPR export. Everything purely informational (a user went offline, a member joined a guild) flows asynchronously over RabbitMQ instead.
Module 5 - Standard user management:
The User Service owns profile data (display name, avatar, banner, status), the friend system (request / accept / decline / block) and online presence broadcasting. It exposes a complete REST API and publishes events when state changes for the rest of the system to consume.
Module 6 - Organization system:
Discord "servers" map directly to organizations: users create guilds, invite members, and organize communication into categories and channels. The Guild Service handles full CRUD for guilds, categories and channels, plus membership management (join via invite, leave, kick, ban).
Module 7 - OAuth 2.0:
The Auth Service implements the OAuth 2.0 Authorization Code flow with three providers: Google, GitHub, and 42. Each OAuth identity is matched strictly by (provider, provider_id) and maps to exactly one account; tokens are exchanged server-side and the frontend never sees provider credentials. Linking multiple providers onto a single existing account (or unlinking one) is a deliberate future feature, out of scope for this submission. Logging in with a second provider under the same email creates a separate account rather than merging into the existing one.
Module 8 - Notification system:
The Notification Service is purely reactive: it consumes RabbitMQ events from Auth, Guild and Chat (user.registered, friend.request_sent, chat.message_sent with mention, chat.dm_sent, call.incoming, etc.), persists a notification row with actor / source / expiry, then pushes a real-time event to the recipient. Notifications survive disconnects (read state is server-side) and respect per-user mute preferences.
Module 9 - Monitoring (Prometheus + Grafana):
Each service exposes a /metrics endpoint scraped by Prometheus. Custom Grafana dashboards track request latency, error rates, active WebSocket connections, RabbitMQ queue depth and service health. Alerting rules notify via a webhook when error rates spike. Access to Grafana is password-protected.
Module 10 - ORM (Entity Framework Core):
The Guild Service uses EF Core 10 with a code-first model (Domain entities in Guild.Domain, configured in Guild.Persistence). EF Core handles relational queries, migrations and the unit-of-work pattern. Other services use lower-level data access tools by choice (the pg driver for User, the Cassandra C# driver for Chat) so this module is explicitly scoped to Guild.
Module 11 - Advanced permissions system:
Each guild has a configurable role system. Roles carry a position (hierarchy), a permission bitmask, an optional color and hoist / mentionable flags. Permission resolution is co-located with guild membership in the Guild Service: a single in-memory pass over the caller's roles plus the channel's permission overwrites resolves the effective bitmask, with short-circuit on ADMINISTRATOR. Owners short-circuit higher than any role, role hierarchy gates role / member edits, and "you can't grant permissions you yourself lack" is enforced server-side.
Module 12 - Voice and video calls (Module of choice, IV.10):
1-on-1 real-time voice and video calling between users, implemented via WebRTC peer-to-peer connections. Call initiation and signaling (SDP offer / answer, ICE candidate exchange) are handled through the existing Chat Service SignalR hub, which means no extra service is required to coordinate calls. A STUN server handles NAT traversal for most network configurations, with a self-hosted coturn TURN server as fallback for restrictive networks.
Why we claim this as a Major under IV.10: this is substantial work (the WebRTC PeerConnection API, ICE negotiation, media-stream lifecycle, call-state machine, offline-callee notification and a full call UI on the frontend), it directly extends a core Discord feature complementing real-time chat, and the implementation touches three layers (Chat Service SignalR hub for signaling, frontend WebRTC client and call UI, coturn deployment for NAT traversal). The complexity and breadth justify treating it as a 2-point Major.
Module 13 - Custom design system:
The frontend ships with a dedicated design system rather than ad-hoc styling. The Tailwind configuration (frontend/tailwind.config.ts) defines a complete color palette (primary-bg, secondary-bg, panel, frame, stroke, plus accent colors aqua, lavender, pink, orange, yellow, lime), a 13-step typography scale with paired line heights, custom spacing tokens, and a signature glow shadow. Typography is driven by three Google fonts (Inter for body, Outfit for category labels, JetBrains Mono for code). Icons come from lucide-react. On top of the tokens we ship 11 reusable components in frontend/src/components/ (auth-card, channel-list, chat-message, chat-workspace, dm-list, friends-list, guild-member-list, guild-sidebar, notification-card, profile-card, settings-modal), all composed from the design system. This satisfies the subject's "minimum 10 reusable components" plus palette / typography / icons requirement.
Module 14 - Server-Side Rendering:
The frontend uses Next.js 16's App Router, which renders every component as a React Server Component by default. Pages opt into client-side interactivity per file by adding "use client"; we do not opt in unless the component genuinely needs hooks or DOM-side event handlers, so static layouts and the initial render of dynamic pages are streamed pre-rendered HTML from the server. This improves first-contentful-paint, gives the markup to crawlers without JS execution (better SEO), and keeps the JavaScript bundle smaller because server-only logic never ships to the client. The subject explicitly flags SSR as incompatible with the ICP blockchain backend, which we do not use.
Module 15 - File upload and management:
Three upload surfaces back this module: user avatars (POST /users/{id}/avatar, multipart, JPEG / PNG / WebP, 5MB cap), profile banners (POST /users/{id}/banner, same constraints), and chat attachments (POST /attachments two-step flow, up to 10 per message, referenced by attachment_ids on POST /channels/{id}/messages and POST /dms/{user_id}/messages). Validation runs on both sides: the frontend rejects bad type or size before upload and shows progress, the backend re-validates and writes to a MinIO container that owns storage end-to-end so clients never see arbitrary URLs. Deletion is supported per surface (DELETE /users/{id}/avatar reverts to the default; deleted messages cascade their attachment refs). A cap of ten attachments per message, per-file type and size validation, and a background reaper that sweeps unreferenced draft uploads keep storage usage bounded. Attachments render inline in the chat view (image preview, click-through download for other types).
Module 16 - Multi-browser support:
Chrome is the subject's mandated baseline; we additionally support Firefox and Safari with broad feature parity. The frontend is built on Next.js + Tailwind which produce standards-compliant output that works cross-browser by default; we add explicit testing passes per browser plus per-browser CSS prefixing where needed. WebRTC behaviour gets dedicated attention since Safari historically diverges (autoplay rules, codec preferences, getUserMedia constraints), and any browser-specific quirks we cannot work around are documented as known limitations alongside the supported-browsers list in the project documentation.
Module 17 - tools/secretman (Module of choice, IV.10):
tools/secretman is a Go CLI that wraps SOPS + age to keep per-service secrets encrypted in Git rather than passed around out of band. It owns the full lifecycle: bootstrapping the age key for a new contributor, registering their public key in .sops.yaml with an authored comment, encrypting / decrypting / refreshing per-service env files, archiving + checksumming the encrypted bundles for distribution, and installing a BetterLeaks pre-commit hook so plaintext secrets are caught before they're committed. The team uses it every day for local-dev secret management; without it we would either commit plaintext secrets (a Hard-Requirement violation per chapter III) or coordinate secrets out-of-band, both of which scale poorly across a five-person project. We claim this as an IV.10 Minor because it is genuinely substantial Go code (multiple commands, key-management UX, hook installation, archive workflow) that solves a real recurring problem in our day-to-day collaboration, rather than a trivial helper script.
Module 18 - GDPR compliance:
The four subject bullets all map to concrete pieces of work:
- Allow users to request their data + Export user data in a readable format. Each service exposes an
/internal/users/{user_id}/data-exportendpoint that returns a JSON blob of the data that service holds about that user. The User Service hosts a public aggregator that fans out to Auth, Guild, Chat and Notification in parallel, stitches the results into a single bundle, and returns it as a downloadable JSON file. The frontend exposes a "Download my data" button in user settings. - Data deletion with confirmation.
DELETE /auth/merequires the frontend confirmation flow (#150) before firing; the Auth Service then publishesuser.deleted, which Auth, User, Guild, Chat and Notification all consume (#146,#147,#148,#149) to purge that user's rows from their own databases. A synchronous Auth to Guild/internal/users/{user_id}/owned-guilds-countcheck (#206, already merged) blocks deletion if the user still owns guilds, forcing them to transfer ownership or delete those guilds first. - Confirmation emails for data operations. A MailHog container in
compose.yamlprovides an SMTP endpoint for the eval environment. The Notification Service grows an email-sending side that consumes theuser.deletedevent (sends "Your account has been deleted") and a newdata.export_readyevent (sends "Your data export is ready, here is the download link"). For production deployments the MailHog container is swapped for a real SMTP provider; the Notification Service code stays the same.
Module 19 - Advanced chat features (IV.6 Minor):
This module enhances the basic chat from the User Interaction module with a set of quality-of-life features, all delivered over the existing Chat Service SignalR hub. Users can block another user, which suppresses direct messages server-side (the Chat Service returns RecipientBlocked after checking the block relationship) as well as mention notifications. Typing indicators are broadcast per channel and per DM, with server-side rate limiting and expiry (ChatHub.Typing). Read receipts are tracked as per-user read state for both channels and DMs and pushed live to other clients (ReadStateUpdated / DmReadStateUpdated). User profiles are reachable directly from the chat view by clicking a message author, and full chat history is persisted in ScyllaDB with cursor-based pagination. Two of the subject's listed sub-features - inviting users to play games from chat, and game/tournament notifications in chat - are game-specific and therefore out of scope for this project, which is a social messaging platform rather than a game; every chat-focused enhancement in the module is implemented. The module's dependency (a working basic chat) is satisfied by the User Interaction Major.
- Maintained and prioritized the product backlog, validated completed features before marking modules as done
- Designed the overall microservices architecture and inter-service communication contracts (REST + RabbitMQ event schemas like
user.registered,chat.message_sent,friend.request_sent,guild.member_joined) - Wrote the full Docker Compose setup (all services, databases, RabbitMQ, NGINX, Prometheus, Grafana, MinIO), configured NGINX reverse proxy and HTTPS/TLS, set up Tilt for local file-watching dev
- Set up RabbitMQ + MassTransit conventions across the .NET services
- Set up Prometheus scraping and built Grafana dashboards with alerting (with jramiro's help on the monitoring side), driven by per-service OpenTelemetry metric exporters
- Implemented the Guild Service (ASP.NET Core, Clean Architecture, EF Core): full CRUD for guilds, channel categories, channels, membership management (invite, join via link, kick, ban) and the full role and permissions system (role CRUD, role hierarchy, permission bitmasks, channel permission overwrites, permission resolution, plus the
/internal/users/{user_id}/owned-guilds-countendpoint that gates Auth's account deletion) - Led the Chat Service (ASP.NET Core + SignalR + ScyllaDB) alongside sliziard and achu: foundations, SignalR hub, channel message send/history, lazy channel subscription, DM send/history, WebRTC signaling (call offer / answer / ICE relay) and the coturn STUN/TURN deployment
- Built the frontend WebRTC calling experience on top of my own signaling hub: the
RTCPeerConnectionlifecycle, trickle-ICE exchange over SignalR,getUserMediacamera/mic handling, the call state machine (ringing / active / ended), and the full call UI (incoming-call overlay, side-by-side video tiles, active-speaker indicator, mute / camera / hang-up controls, and a minimizable in-call window) - Added the MailHog container to the compose stack for GDPR confirmation emails, and implemented Guild + Chat's
/internal/users/{user_id}/data-exportendpoints anduser.deletedcascade consumers
Challenges:
Most of the hard problems in my work came down to correctness under concurrency and across service boundaries, which is a polite way of saying "things that worked perfectly until two people clicked at once :D" The worst offender was the Guild Service's transactional outbox: domain events had to publish only if their database transaction committed, because a crash in the half-second between "row saved" and "event sent" silently desyncs every downstream service and never tells you it happened. Getting the MassTransit outbox to enlist in the same EF transaction and drain reliably took real iteration. Closely related was optimistic concurrency, where I map EF's xmin version tokens and duplicate-key insert races onto a clean 409 Conflict instead of a 500, so two clients editing the same guild or role resolve into correct semantics instead of a torn write.
The permission resolver was hard in a different way: effective access is a single in-memory pass over the caller's roles and channel overwrites, with owner and ADMINISTRATOR short-circuits, hierarchy gating on edits, and a "you can't grant a permission you don't hold yourself" rule, and making all those short-circuits compose correctly forced a consolidating refactor into AuthorizationContext + PermissionResolver. On the messaging side, cross-service events kept vanishing into *_skipped queues, MassTransit's serene way of telling you a message will never be delivered and declining to explain why, until I learned every consumer needs a matching [MessageUrn]; after that I single-sourced the snake_case policy so the envelope serialized identically over HTTP and MassTransit.
The data and real-time layers had their own traps. ScyllaDB partitions messages by channel (and DMs by conversation), so "give me everything belonging to one user" (read-state, DM lists, the GDPR export) is a full-cluster scan unless you plan for it, so I added per-user index tables and cursor-based pagination to keep those reads single-partition; Scylla is plenty fast, but only if you ask it questions the way it likes to be asked. WebRTC was the other one: driving 1-on-1 call setup over the SignalR hub with no media server meant running the whole ring/answer/hangup/timeout state machine myself, guarding against duplicate call_ids on a re-offer and scoping the timeout to the caller's leg so a call stops ringing into the void instead of forever. And holding all of it up, keeping one Compose stack running identically on both Docker and Podman (two container engines that agree on remarkably little: rootless quirks, port wiring, healthcheck differences) so anyone could bring the whole thing up with one command was its own quiet war of attrition.
- Built the User Service foundation with NestJS, including its module structure, configuration, Docker setup, database integration and development workflow.
- Implemented public profile retrieval and authenticated user access through a JWT guard and a current-user decorator.
- Added profile update functionality with DTO validation for editable user information.
- Implemented user lookup features, including individual profile queries, user searches and batch lookups by user IDs.
- Built the friendship system with friend request creation, listing, updating and relationship persistence.
- Implemented the blocking system with dedicated repositories, endpoints and service-layer handling.
- Added RabbitMQ relationship events so friendship changes can be consumed by other services.
- Implemented profile media storage using MinIO, including the storage layer, persistence repository and configuration.
- Added avatar and banner upload endpoints with backend validation and profile media management.
- Implemented the User Service data-export endpoint for GDPR compliance.
- Built the cross-service GDPR export aggregator, including export generation, storage and communication with the other services.
- Added user lifecycle event handling for profile creation and cleanup when users are registered, deleted or otherwise updated.
- Set up the initial Next.js frontend structure, including authentication, chat, guild, notification and profile pages.
- Implemented login and registration forms with client-side validation and OAuth provider buttons.
- Developed the main chat interface, including guild navigation, channel navigation, member lists, message display and responsive layouts.
- Implemented message editing interactions with keyboard shortcuts, hover actions and improved editing flows.
- Added direct-message navigation, dedicated DM headers, conversation previews, empty states and per-conversation drafts.
- Implemented friend lists, friend request interactions, member profiles and profile panels in the frontend.
- Created typed frontend API modules for the User, Guild, Chat and Notification services.
- Added centralized bearer-token injection to keep authenticated API requests consistent across the application.
- Developed and maintained the frontend design system with shared Tailwind styling, typography, spacing, colors and reusable UI components.
Challenges:
The main challenge was synchronizing a frontend that was being developed alongside several independently evolving backend services. Every new feature required the frontend routes, request payloads, response types and error handling to remain aligned with the corresponding backend contracts.
Authentication was another important integration challenge. The frontend had to manage authenticated sessions while calling protected User Service endpoints through JWT bearer tokens. Centralizing token injection in the API client helped avoid inconsistent authentication behaviour across profiles, friendships, blocking, media uploads and GDPR-related requests.
The User Service also contained several features that affected multiple parts of the application. Profile updates, friend requests, blocks, avatar uploads and banner uploads all required coordination between controllers, DTOs, repositories, storage and frontend forms. A change in one API contract could therefore affect both the backend service and several independent frontend components.
The friendship and blocking systems required the frontend to represent different relationship states correctly, including pending requests, accepted friendships and blocked users. These states also had to remain consistent when the data was updated through different views, such as a profile page, the friends list or a direct-message interface.
The direct-message interface introduced additional synchronization concerns. Conversation selection, message previews, drafts, profile information and chat state had to remain consistent while navigating between guild channels and private conversations.
The GDPR export flow was challenging because the User Service had to gather data from several backend services and expose it through a single user-facing flow. The frontend therefore had to handle a process whose result depended on multiple service responses, export generation and file storage.
Finally, the frontend needed to remain responsive while new dynamic behaviour was added. Components such as the guild sidebar, channel list, member list, chat workspace, profile panels and authentication cards had to be progressively refactored without breaking existing layouts or interactions.
- Owned project management end-to-end: ran sprint planning and grooming sessions, populated and maintained "The Holy Board" on GitHub Projects across five sprints, broke epics down into well-scoped issues, tracked iteration progress and blockers, drove retros, kept the team's communication rhythm on Discord
- Set up the Auth Service from scaffolding to production-shaped Clean Architecture:
Auth.Domain(entities, value objects likeEmailandOAuthIdentity, failure catalogue),Auth.Application(command/query handlers, abstractions),Auth.Infrastructure(JWT issuance, OAuth provider clients, password hasher),Auth.Persistence(EF Core mappings, migrations),Auth.Presentation(Carter endpoints, global exception middleware) - Implemented the email + password auth flows:
POST /auth/registerandPOST /auth/loginwithbcrypt-hashed + salted passwords,POST /auth/refreshwith refresh-token rotation and hashed storage of refresh tokens,POST /auth/logoutwith refresh-token revocation,GET /auth/mefor the authenticated principal's auth-side identity, and soft-delete onusers_authso emails free up correctly after deletion - Implemented the full OAuth 2.0 Authorization Code flow for three providers (Google, GitHub, 42) under
GET /auth/oauth/{provider}andGET /auth/oauth/{provider}/callback, plus the integer-encodedoauth_providerstorage decision (Github=1,Google=2,FortyTwo=3) so the schema stays compact and migrations don't churn on enum renames - Implemented the synchronous Auth to Guild
/internal/users/{user_id}/owned-guilds-countcheck that gatesDELETE /auth/meand forces users to transfer or delete owned guilds before account deletion; published theuser.deletedevent that drives the GDPR deletion cascade across User, Guild, Chat and Notification - Added Auth's
/internal/users/{user_id}/data-exportendpoint for the GDPR data-export bundle (email, OAuth providers, account timestamps), validated that OAuth-only accounts cannot be tricked into changing their email or password via/auth/me - Co-developer on the Chat Service alongside yandry and achu, contributing to the SignalR hub, message flows, connection tracking and consumer wiring as part of the cross-team push to round out the chat experience.
Challenges:
My first real challenge was learning C# itself. Coming from other languages the syntax wasn't too disorienting once I got past the sheer number of keywords, but the bigger hurdle was the .NET ecosystem around it: dependency injection, expressing business ports as interfaces, and pulling that apart into Domain / Application / Infrastructure / Persistence / Presentation, apply hexagonal architecture in practice, not just in theory.
On the Auth Service specifically, understanding the access/refresh token split took a moment to click (it's simple once you see it, abstract until then), as did the reasoning behind hashing refresh tokens differently from passwords (SHA-256 for the opaque refresh token vs. bcrypt for passwords, since one is a system-generated random secret you only ever compare, the other is user-chosen and needs to resist offline brute-forcing).
Reading through three different OAuth providers' documentation to reconcile their quirks into one Authorization Code flow was its own exercise in patience.
EF Core was harder than expected in the other direction, its implicit conventions and change-tracking behavior aren't obvious until something surprises you. But genuinely pleasant once properly configured and understood. And serialization kept coming back as a recurring theme: getting the same C# classes to translate consistently to the right wire format across three different transports (HTTP JSON, SignalR, RabbitMQ events) took more care than I expected going in.
As Project Manager, this was less a technical challenge and more a coordination one; yandry owned the product side (contracts, issues, the board) while I focused on keeping developers coordinated: splitting up tasks, running team meetings, and keeping everyone's work visible to everyone else. We underestimated the project's timeline early on, aimed a bit too big, and lost time getting lost in details before a round of team meetings forced us to refocus the scope.
On Chat, SignalR itself was a big learning curve: the connection-per-user paradigm, authenticating a WebSocket via a JWT passed in the query string, and the operational reality of tracking many concurrent connections per user turned out to be genuinely hard to get right, UserConnectionTracker ended up being the fix for several separate blocking issues at once, and getting there meant revisiting our own contract: we dropped an early plan to auto-join every channel a user belonged to on connect, in favor of explicit client-driven subscribe/unsubscribe per channel. Message storage was another sore spot: channel and DM messages started out as two parallel Cassandra-backed tables duplicated almost line-for-line in the C# service (attachments especially), which is what eventually forced a large refactor PR (#289) to unify them and cut the repetition. Cross-service interactions were also a real open question for a while; the pattern we settled on (internal-only /internal/... endpoints, jramiro's idea) is what let me build the Guild-side GET /internal/users/{user_id}/channels endpoint Chat needed, keeping the DB/inter-service traffic bounded instead of resolving membership per-guild on every call.
Finally, a smaller but real personal challenge: learning to work with GitHub project management properly, use the board ("The Holy Board") with categorized issues, iterations and statuses, and the PR workflow itself. It took real time to get comfortable with git and GitHub at first, but our workflow today feels genuinely clean, and I kept learning throughout the project (git history/manipulation, GitHub interactions, reviewing, branch organization).
jramiro - Art Direction / Frontend Wireframes / API Gateway / Secret Management / Infra Support / Terms of Service
- Led the frontend art direction and produced the first wireframes for the main views, which served as the visual and UX baseline for later implementation
- Implemented the API Gateway service in Go, including
/api/{service}/vN/...routing, local JWT validation, dual rate limiting (per-IP and per-UID), OpenAPI-based request validation, and timeout middleware - Designed and built
tools/secretmanfor encrypted secret workflows with SOPS: ensure/encrypt/decrypt/refresh flows, age key bootstrap for new contributors,.sops.yamlupdates, archive + checksum helpers, sensitive output masking, and pre-commit leak-prevention hook setup - Provided targeted infrastructure support with Yandry
- Drafted and integrated the project's Privacy Policy and Terms of Service pages
Challenges:
- Life
- Implemented the Notification Service in Go from scaffolding to production: PostgreSQL-backed persistence with the
notificationstable (actor, source, type, expiry, dismiss tracking) andnotification_preferencestable for per-user mute (per-guild and per-channel granularity) - Wired all five upstream consumers:
user.registered(welcome notification),friend.request_sent(friend_request type),chat.message_sentwith mention detection (mention type),chat.dm_sent(dm type),call.incoming(incoming_call type), anduser.deleted(cascade purge of the user's notifications and preferences) - Built the real-time delivery layer: pushed notifications to connected clients over a Server-Sent Events stream (
GET /notifications/events), emitting a namedReceiveNotificationevent that updates navbar badges without polling - Built the REST surface (
GET /notifications,POST /notifications/{id}/read, mute preference endpoints) plus the background cleanup for old dismissed notifications (#171) - Wired mention block-suppression to the User Service's internal endpoint (
#191) so users who have blocked each other do not trigger mention notifications - Implemented Notification's
/internal/users/{user_id}/data-exportendpoint and the email-sending side of the Notification Service: consumesuser.deletedanddata.export_readyevents, renders the corresponding confirmation emails (plaintext + minimal HTML templates) and dispatches them through MailHog locally / a real SMTP provider in production
Challenges:
The Notification Service is a passive consumer. Almost all of its input arrives from the broker, which means I needed real control over what happens to each message, so I disabled auto-ack and took on the responsibility of acking myself. Not every failure deserves the same outcome, for example a transient one, if the databse is down or the user service in unreachable, this should be nacked and requeued, while a malformed payload is a poison message that will loop forever if I put it back. Drawing that line wasn't a decision I could make alone, because the events come from my teammates' services, so the taxonomy was arbitrated with the team. The service also has no deduplication, and that's an identified limit rather than an oversight, if the mail fails to send after the notification has already been persisted, a redelivery recreates it. There is more than one flaw in my service, I know where the they are and why they exist, but it fell outside the scope we could reasonably cover, they are written in my service so we may one day come back and implement/fix them.
The contract originally called for a SignalR WebSocket for real-time delivery, and I pushed back, a notification stream is unidirectional, and paying for a full duplex connection to send in one direction is cost without benefit, so I proposed Server-Sent Events instead, and the change was accepted quickly. But is brought its own problem, the consumer runs on its own goroutine, each SSE connection is another goroutine blocked on its ResponseWriter, and routing a notification from one to the other means keeping a registry of live connections per user, fanning out to every open tab, and cleaning up on r.Context().Done() when the client disconnects, or the goroutines and channels leak. Authentication had to move to the query string, since the browser's EventSource API won't let you set custom headers. SSE is a layer on top of the database, not dependent on it, a user who's offline loses nothing, the notification is persisted and picked up on the next fetch.
I chose sqlc over an ORM, the SQL is written by hand and sqlc generates typed Go from it, so there's no change-tracking behaviour to be surprised by and no ambiguity about which query actually runs. That paid off on the GDPR deletion, which has to clear a user's notifications and their preferences together, either both tables are cleaned or neither is. Wrapping both in one transaction with the rollback deferred as a safety net made that guarantee cheap to express and easy to read. Notification preferences are scoped to guilds and channels only, which is a deliberate limit rather than an unfinished feature, and old notifications are purged after seven days by a ticker running in its own goroutine.
That restraint was itself part of the work. The consumer runs on a single goroutine, and the design allows more if the volume ever calls for it, but nothing in what we actually do calls for it today. The fan-out is bounded by construction: a mention produces one notification per mentioned member, not one per member of the channel, so the work generated by any single event stays small and predictable. Deciding what genuinely needed to scale, what could scale later, and what didn't need to scale at all turned out to be a real part of building this service, and resisting the temptation to over-engineer the parts that didn't need it was harder than it sounds.
Almost everything about this project was new to me at the start: Go, RabbitMQ and, more importantly how to work as one person inside a group project. The real challenge was never the stack. Go itself turned out to be a good place to learn backend work: simple to write, easy to read, and well suited to it thanks to the goroutines net/http spawns per request. What took longer was learning to think about a service as one piece of a system I didn't control, and to work with a team through issues, pull requests and shared conventions rather than around them.
