Technology summary
| Aspect | Choice |
|---|---|
| Runtime | .NET 8 |
| Framework | ASP.NET Core Web API |
| Language | C# with nullable reference types |
| Serialization | Newtonsoft.Json (camelCase, ignore nulls, UTC dates) |
| Auth | JWT Bearer (Supabase-issued, HS256) |
| API Docs | Swashbuckle / Swagger |
| Database Client | Raw HTTP calls to Supabase PostgREST API |
| DI Container | Built-in Microsoft.Extensions.DependencyInjection |
Controller map
Every controller inherits fromControllerBase (no views). Routes use attribute routing.
Public API controllers
| Controller | Route prefix | Auth | Purpose |
|---|---|---|---|
HealthController | /health, /api/health | Anonymous | Health checks |
ArenaController | /api/arena | Bearer JWT | Chat, dual chat, streaming |
ModelsController | /api/models | Bearer JWT | List active AI models |
ThreadsController | /api/threads | Bearer JWT | Thread CRUD + messages |
UsersController | /api/users | Bearer JWT | User sync |
SettingsController | /api/settings | Anonymous | Feature flags |
SpeechController | /api/speech | Bearer JWT | Text-to-speech |
Admin API controllers
All admin controllers are under/api/admin/ and use IAdminSupabaseClient for data access with service role key.
| Controller | Route prefix | Purpose |
|---|---|---|
AdminDashboardController | /api/admin/dashboard | Stats, activity, performance |
AdminAIModelsController | /api/admin/models | AI model CRUD |
AdminUsersController | /api/admin/users | User CRUD |
AdminComparisonsController | /api/admin/comparisons | Comparison CRUD |
AdminModelVotesController | /api/admin/votes | Vote CRUD + stats |
AdminThreadsController | /api/admin/threads | Thread CRUD |
AdminThreadMessagesController | /api/admin/messages | Message CRUD |
ProvidersController | /api/admin/providers | Provider + API key management |
ArenaController — the core
TheArenaController (Controllers/Api/ArenaController.cs) is the heart of DualMind. It handles all chat operations.
Dependencies injected
Single chat flow (POST /api/arena/chat)
- Validate prompt is not empty
- Select model: if
request.Modelis"auto"or null, call_modelSelector.GetRandomModelAsync(); otherwise use the specified model - Call
ExecuteWithFallbackAsync(model, prompt, system, maxTokens, temperature) - Build
ChatResponsewith output content, model info, usage stats, response time - Log message via
_messageLogger.LogMessageAsync() - If
request.ThreadIdis set, persist to thread via_threadMessagesService.LogSingleAsync() - Return response
Dual chat flow (POST /api/arena/dualchat)
- Validate prompt
- Determine selection mode:
- Manual: Both
model1andmodel2specified by client - Topper:
_leaderboardModelSelector.GetTopperAndRandomModelAsync()— top-rated model vs random - Random (default):
_modelSelector.GetTwoRandomModelsAsync()
- Manual: Both
- Execute both models in parallel via
Task.WhenAll(task1, task2) - Build two
ChatResponseobjects - Log both messages and the comparison
- Compute arena metrics (winner by length, winner by tokens, verdict)
- Return
{ agent1, agent2, comparisonId, arena: { comparison, models } }
Fallback logic (ExecuteWithFallbackAsync)
Service layer
All business logic is inCore/Services/. Services are registered as Scoped except ModelSelector which is Singleton.
| Service | Interface | Responsibility |
|---|---|---|
ModelSelector | IModelSelector | Query active models from DB, random selection, model info lookup |
LeaderboardModelSelector | ILeaderboardModelSelector | Select top-rated model + random opponent |
ThreadsService | IThreadsService | Thread CRUD, visibility management |
ThreadMessagesService | IThreadMessagesService | Log single/dual messages to threads |
ModelStatsService | IModelStatsService | Voting statistics, win rates |
ComparisonLogger | IComparisonLogger | Persist comparison records |
MessageLogger | IMessageLogger | Persist individual chat messages |
UserSyncService | IUserSyncService | Ensure public.users row exists before FK operations |
SystemSettingsService | ISystemSettingsService | Feature flag queries |
ProviderConfigService | IProviderConfigService | API key rotation, cooldowns, error tracking |
Data access layer
DualMind does not use Entity Framework. It makes direct HTTP calls to the Supabase PostgREST API.ISupabaseService (user-facing)
Used by public controllers. Configured with service role key in HttpClient default headers.
IAdminSupabaseClient (admin-facing)
Used by admin controllers. Provides generic CRUD operations:
Middleware pipeline
Configured inProgram.cs, executed in order:
- Exception handler — catches unhandled exceptions, returns
ProblemDetailsJSON - Request logging — logs correlation ID, method, path, duration, status code
- CORS —
AllowAllpolicy (any origin, method, header) - HTTPS redirection
- Authentication — JWT Bearer validation
- Authorization —
[Authorize]attribute enforcement - Controller routing —
app.MapControllers()
Error response format
All errors follow a consistent shape:INVALID_REQUEST, API_ERROR, STREAM_ERROR, THREADS_ERROR, THREAD_CREATE_ERROR, MODELS_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN.