Architecture Overview¶
PoracleWeb is a full-stack application with a .NET 10 backend API and Angular 21 frontend SPA.
Solution structure¶
Pgan.PoracleWebNet.slnx
├── Applications/
│ ├── Web.Api/ ASP.NET Core host
│ │ ├── Controllers/ REST API controllers (all under /api/)
│ │ ├── Configuration/ DI registration, settings classes
│ │ └── Services/ Background services (avatar cache, DTS cache)
│ └── Web.App/ClientApp/ Angular 21 SPA
│ └── src/app/
│ ├── core/ Guards, services, interceptors, models
│ ├── modules/ Feature pages (dashboard, pokemon, raids, etc.)
│ └── shared/ Reusable components, utilities
├── Core/
│ ├── Core.Abstractions/ Interfaces (IService, IPoracleTrackingProxy, IPoracleHumanProxy)
│ ├── Core.Models/ DTOs passed between layers
│ ├── Core.Mappings/ AutoMapper profiles (Human, Profile, PoracleWeb tables)
│ ├── Core.Repositories/ Data access (Human, Profile, PoracleWeb-owned tables)
│ └── Core.Services/ Business logic + PoracleNG API proxies
├── Data/
│ ├── Data/ EF Core DbContexts, Entities, Configurations
│ └── Data.Scanner/ Optional scanner DB context (RDM)
└── Tests/
└── Pgan.PoracleWebNet.Tests/ xUnit backend tests
Layer diagram¶
graph TB
subgraph Frontend
Angular[Angular 21 SPA
Material Design 3]
end
subgraph Backend["ASP.NET Core API"]
Controllers[Controllers
20+ REST endpoints]
AlarmServices[Alarm Services
MonsterService, RaidService, etc.]
TrackingProxy[IPoracleTrackingProxy
HTTP proxy to PoracleNG]
HumanProxy[IPoracleHumanProxy
HTTP proxy to PoracleNG]
OtherServices[Other Services
Geofences, Settings, QuickPicks]
Repositories[Repositories
Admin bulk ops, PoracleWeb DB]
end
subgraph Data
PoracleDB[(Poracle DB
MySQL)]
WebDB[(PoracleWeb DB
MySQL)]
ScannerDB[(Scanner DB
RDM · Optional)]
end
subgraph External
PoracleNG[PoracleNG API
Alarm tracking + state reload]
Koji[Koji Server]
Discord[Discord API]
end
Angular -->|HTTP / JWT| Controllers
Controllers --> AlarmServices
Controllers --> OtherServices
AlarmServices --> TrackingProxy
Controllers --> HumanProxy
TrackingProxy -->|HttpClient
X-Poracle-Secret| PoracleNG
HumanProxy -->|HttpClient
X-Poracle-Secret| PoracleNG
PoracleNG --> PoracleDB
OtherServices --> Repositories
Repositories --> PoracleDB
Repositories --> WebDB
Repositories --> ScannerDB
OtherServices -->|HttpClient| Koji
OtherServices -->|HttpClient| Discord
All operations go through PoracleNG
All alarm tracking CRUD is proxied via IPoracleTrackingProxy. Single-user human/profile operations (reads, creates, location, areas, profile switch) are proxied via IPoracleHumanProxy. Direct database access is only used for admin bulk operations (GetAllAsync, DeleteUserAsync, UpdateAsync) and application-owned data (poracle_web database). See PoracleNG API Proxy for details.
Key design decisions¶
Operations proxied through PoracleNG¶
All alarm tracking writes (create, update, delete) and single-user human/profile operations go through the PoracleNG REST API, not directly to the database. PoracleNG applies field defaults (cleanRow()), detects duplicates, handles area dual-writes, and triggers immediate state reload. This eliminates data integrity bugs caused by missing defaults or stale state. See PoracleNG API Proxy.
Separate databases¶
PoracleWeb does not modify the Poracle DB schema. The Poracle database is managed by PoracleNG. Application-owned data (user geofences, site settings, webhook delegates, quick pick definitions) lives in a separate poracle_web database managed by EF Core migrations.
Unified geofence feed¶
PoracleWeb acts as the single geofence source for PoracleJS. It fetches admin geofences from Koji, merges them with user-drawn geofences, and serves everything via one endpoint (GET /api/geofence-feed). No custom code needed in PoracleJS or Koji.
AutoMapper for partial updates¶
All update models use nullable int? properties so partial updates don't zero out unset fields. The mapping profile skips null properties automatically. Note: AutoMapper is now only used for non-alarm entities (humans, profiles). Alarm data flows as raw JSON through the PoracleNG API proxy.
Gym picker¶
The GymPickerComponent (shared) lets users search for specific gyms when creating team, raid, or egg alarms. It calls the ScannerService (frontend) which hits scanner gym search endpoints on the backend (ScannerController). Search results use the GymSearchResult model and include photo thumbnails and area names resolved via the PointInPolygon geo utility. The scanner DB is optional — when not configured, the gym picker is hidden.
Per-IP rate limiting¶
Auth endpoints use per-IP partitioned rate limiting (not global). This prevents one user's activity from locking out others.