Loopless is a full-stack SaaS platform (Next.js 15 frontend + .NET 10 backend) deployed via Docker Compose (dev) and Kubernetes + Helm (prod). All traffic enters through Nginx; the frontend and backend are independently scalable.
flowchart TB
subgraph Client["Client Layer"]
Browser[User Browser]
end
subgraph Gateway["Gateway Layer"]
Nginx[Nginx Reverse Proxy\nport 80 / 443]
end
subgraph Frontend["Frontend (Next.js 15)"]
NextApp[Next.js App Router\nport 3000]
TanStack[TanStack Query]
Zustand[Zustand Store]
SignalRClient[SignalR Client\nexponential backoff]
PostHog[PostHog Analytics]
end
subgraph Backend[".NET 10 API (Clean Architecture)"]
API[Minimal API\nEndpoint Extensions]
MediatR[MediatR CQRS\nValidation + Logging Behaviors]
Hangfire[Hangfire\n4 recurring jobs]
SignalRHub[SignalR Hubs\nMessagingHub + NotificationsHub]
RateLimiter[Rate Limiter\nFixed + Auth policies]
CorrelationID[Correlation ID\nMiddleware]
end
subgraph Data["Data Layer"]
Postgres[(PostgreSQL 16\n+ pgvector)]
Redis[(Redis 7\nCache + SignalR backplane)]
RabbitMQ[(RabbitMQ 3\nTopic exchange)]
S3[(MinIO / S3\nFile storage)]
end
subgraph Identity["Identity"]
Keycloak[Keycloak 25\nOAuth2 / OIDC\nGitHub SSO]
end
subgraph AI["AI Services"]
OpenAI[OpenAI API\nEmbeddings + Chat]
end
subgraph Observability["Observability"]
Prometheus[Prometheus 2.54\nMetrics scrape]
Grafana[Grafana 11\nDashboards]
Loki[Loki 3.1\nLog aggregation]
Elasticsearch[Elasticsearch 8.14]
Kibana[Kibana 8.14]
UptimeKuma[Uptime Kuma\nStatus page]
Alertmanager[Alertmanager\nWebhook → AIOps]
AIOps[aiops-triage\nOpenAI → Slack]
end
subgraph Automation["Automation"]
N8N[n8n\nWorkflow automation\nport 5678]
end
Browser --> Nginx
Nginx --> NextApp
Nginx --> API
NextApp --> TanStack
NextApp --> Zustand
NextApp --> SignalRClient
NextApp --> PostHog
SignalRClient --> SignalRHub
API --> MediatR
API --> Hangfire
API --> SignalRHub
API --> RateLimiter
API --> CorrelationID
MediatR --> Postgres
MediatR --> Redis
MediatR --> RabbitMQ
MediatR --> OpenAI
MediatR --> S3
API --> Keycloak
SignalRHub --> Redis
Prometheus --> Grafana
Loki --> Grafana
Elasticsearch --> Kibana
API --> Prometheus
API --> Loki
API --> Elasticsearch
Prometheus --> Alertmanager
Alertmanager --> AIOps
AIOps --> OpenAI
N8N --> API
| Context | Responsibility | Key Entities |
|---|---|---|
| Identity | Auth, RBAC, session | User, KeycloakId |
| Matching | Semantic freelancer discovery | FreelancerProfile, Embedding, MatchScore |
| Projects | Project lifecycle + GitHub sync | Project, GitHubCommit, ProjectSummary |
| Messaging | Real-time DMs | Conversation, Message |
| Standups | Async daily check-ins + blocker detection | Standup, BlockerFlag |
| Notifications | In-app + email delivery | Notification, EmailNotification |
| Analytics | Platform-level metrics | AuditTrail, PostHog events |
Browser
→ Nginx (port 80)
→ Next.js GET /discover
→ TanStack Query → axios → GET /api/v1/matching
→ .NET API (rate limiter → correlation ID → JWT validate → MediatR)
→ GetMatchesQuery handler
→ Redis cache check (miss)
→ PostgreSQL pgvector cosine similarity
→ OpenAI embedding (if new query)
→ result cached in Redis (TTL 5 min)
→ response: MatchResultDto[]
→ MatchCard components rendered
| Environment | Stack | Notes |
|---|---|---|
| Dev | Docker Compose (16 services) | Hot reload, mock auth |
| Staging | K8s + Kustomize overlay | Image from GHCR, staging secrets |
| Production | K8s + Helm chart + HPA | TLS via cert-manager, Azure Key Vault secrets |
| Requirement | Target | Mechanism |
|---|---|---|
| API latency (p95) | < 200ms | Redis cache-aside, pgvector IVFFlat index |
| Throughput | 60 req/min per IP | Fixed window rate limiter |
| Availability | 99.5% | Uptime Kuma monitors, K8s HPA (min 2 replicas) |
| Observability | Full trace | Correlation ID on every request, Serilog → Loki |
| Auth brute force | Max 10 req/min on /auth | Stricter rate limit policy on auth endpoints |
Loopless keeps project commit history in sync with GitHub via two complementary mechanisms.
When a project has a github_webhook_secret configured, GitHub delivers push events to:
POST /api/v1/projects/{id}/github/webhook
The endpoint:
- Reads the raw request body and validates the
X-Hub-Signature-256HMAC-SHA256 header against the per-projectgithub_webhook_secret. - Returns
401 Unauthorizedon signature mismatch — no detail leaked. - Acknowledges non-
pushevent types with200 OK(no-op). - On a valid
pushevent, immediately enqueues aSyncGithubCommitsJobvia Hangfire — commits appear within seconds of the push.
The endpoint is unauthenticated (no JWT required); the HMAC signature is the sole authentication mechanism. The per-project secret is stored in the github_webhook_secret column (VARCHAR 256, nullable) added by the AddGithubWebhookSecret migration.
A Hangfire recurring job (github-commit-sync, GitHubSyncJob.SyncAllAsync) polls all projects with a configured GitHubRepoUrl every 6 hours as a catch-all for missed webhooks (webhook not yet configured, transient GitHub delivery failure). On GitHub rate-limit responses (429 or 403 with X-RateLimit-Remaining: 0), the job reschedules itself 30 minutes into the future.
GitHub push
→ POST /api/v1/projects/{id}/github/webhook
→ HMAC-SHA256 validate (X-Hub-Signature-256 vs project.GitHubWebhookSecret)
→ Hangfire enqueue: SyncGithubCommitsJob (immediate)
→ GitHubService.FetchCommitsAsync → store new commits
→ enqueue SummaryGenerationJob
Fallback (every 6 h):
GitHubSyncJob.SyncAllAsync
→ SyncProjectAsync per project
→ same fetch + store + summary pipeline