Mallo
Community, Conversation & Connection
A Swedish-language community platform built by a 9-person cross-functional team for Chas Challenge 2026 — the annual competition at Chas Academy. Members share posts, find local activities on a map, and chat in real time. I owned DevOps: the Kubernetes deployment, the GitLab CI/CD pipeline, and the observability stack.
What is Mallo?
Mallo is a Swedish-language web community. Members write posts, react with "hugs", comment, save favourites, and find real-world activities pinned to a map of Sweden. A built-in chat lets them message each other directly. It is built mobile-first, with a friendly marshmallow mascot tying the brand together.
"Chas Challenge 2026's theme was Tech for Good — build a product that creatively uses technology to generate clear and meaningful benefit for people, society, or the environment. We answered with a place for community, conversation and connection."
Eighteen cross-functional teams competed. Each team mirrors a real workplace — frontend, fullstack, DevOps, systems development, and UX/UI — and is judged by an external industry jury on how well it executes the theme. Our team, GR8, shipped Mallo end-to-end: a React 19 SPA, a .NET 10 API, MS SQL Server, and real-time chat over SignalR, all running on the school's Kubernetes cluster.
The Product
Mobile-first views. Tap any screen to enlarge.
What it does
Forum
Posts with titles, 13 categories and 20 tags, threaded comments, "hugs" reactions, bookmarks, and user-driven reports for moderation.
Activity map
Geo-tagged events with images, date ranges, and addresses on an interactive Mapbox map. Distance helpers power proximity filtering.
Real-time chat
One-to-one direct messages over a SignalR hub. Typing indicators, read-receipts, per-user soft-delete, and auto-reconnect.
Accounts & auth
Register with Swedish personnummer + 18+ check, JWT bearer tokens, email-based password reset, profile editing, and GDPR account deletion.
Push notifications
A Firebase Cloud Messaging service worker in the browser delivers activity and chat alerts.
My Role — DevOps
Two of us owned DevOps. My focus: getting the whole stack running reliably on the school's k3s Kubernetes cluster, wiring up a GitLab CI/CD pipeline that builds, deploys and smoke-tests every change, and standing up Prometheus + Grafana observability so we could see what the API was doing in production.
Architecture Overview
The SPA and the API deploy separately so they can scale on their own. Everything lives in one
Kubernetes namespace (doe25-group-8), behind a single Traefik ingress.
Browser → Traefik ingress
The React SPA talks to the cluster over HTTPS. Traefik terminates TLS and routes by
path: / to the frontend, /api and /chat/hub
to the API.
Frontend pod (nginx)
The production React build is served by nginx with an SPA fallback
(try_files $uri /index.html).
API pod (.NET 10 + SignalR)
REST endpoints plus the SignalR hub for chat. Auth is a stateless JWT, so the API can scale to more than one replica. SignalR connections are pinned to a single pod via a sticky cookie at the ingress, which keeps chat from dropping.
Durable state: MS SQL + Redis
MSSQL is the only thing with durable state worth worrying about — it runs as a StatefulSet on a Longhorn PVC. Redis (a Bitnami Helm sub-chart) handles caching with a small PVC of its own.
Observability: Prometheus → Grafana
The API exposes /metrics. Prometheus scrapes it; Grafana visualises it;
PrometheusRule fires alerts when something looks wrong.
Running on Kubernetes
Everything ships to the school's k3s cluster (same APIs as upstream Kubernetes, smaller install). We split the deploy in two so it stays approachable for the whole team:
Plain YAML for the app
Frontend, API and MSSQL are plain Kubernetes manifests — anyone on the team can read a
Deployment without learning Helm templating first. The manifests bake in a
gr8-plain placeholder; CI rewrites it per environment
(sed -i 's|gr8-plain|gr8-<env>|g').
Helm for the versioned bits
Redis (an upstream Bitnami sub-chart) and the observability glue — ServiceMonitor, PrometheusRule, and the Grafana dashboard ConfigMap — ride in a Helm chart, where templating earns its keep for release naming.
Secrets stay out of git
Two plain Kubernetes Secret objects — one for app secrets, one for the
image-pull credentials — are created once with kubectl create secret when
a namespace is bootstrapped. CI never templates them and they never land in the repo.
CI/CD Pipeline (GitLab)
Every push runs one pipeline; when a merge request is open, the MR pipeline gates the merge button so deploy and smoke-test have to pass before code reaches an environment.
check-merge-source enforces that only develop can
merge into main, alongside the frontend/backend test
jobs.
GitLab's built-in secret scanning runs on every pipeline.
Docker buildx with registry cache; images pushed to the school registry tagged per branch.
Applies the rewritten manifests, then kubectl rollout restart to force
a fresh pull even when the tag is unchanged; helm upgrade --install
for Redis + observability.
Polls /api/openapi/v1.json every 5s for 5 minutes — long enough to
cover a cold MSSQL pod re-applying 28 EF migrations (~3–4 min) after a fresh
deploy. Only gateway errors and connection failures count as down.
Branches → environments
| Branch | Tag | Deploy |
|---|---|---|
| main | :latest | Manual approval gate · persistent DB |
| develop | :develop | Auto-deploy on push · persistent DB |
| feature/* | :<slug> | Image built, not auto-deployed |
Observability
The school cluster already runs Prometheus + Grafana. Our Helm chart layers Mallo-specific pieces on top so the API isn't a black box in production.
ServiceMonitor
Scrapes the API's /metrics endpoint every 15 seconds.
PrometheusRule
Two alerts: Gr8ApiHighErrorRate (5xx above 5% for 5 min) and
Gr8ApiNoTraffic (0 req/s for 15 min).
Grafana dashboard
Auto-provisioned by a ConfigMap. Four panels: request rate by endpoint, 5xx rate, p95 latency by endpoint, and request count by HTTP status code.
Tech Stack
Team GR8
Nine people across five competencies. I handled DevOps alongside Reza.