Service Communication¶
Mercury is a multi-service system. Services communicate over HTTP (via the YARP gateway or direct internal calls), SignalR WebSockets, and shared PostgreSQL databases. This page maps the full topology and explains each communication pattern.
Service topology¶
graph TB
subgraph Clients
Browser[Browser]
Desktop[Medicine Desktop]
StoreFE[Mercury Store<br/>Next.js]
end
subgraph Gateway
YARP[Mercury.Gateway<br/>YARP Reverse Proxy]
end
subgraph Backend
Portal[Portal.Web<br/>Blazor Server]
Identity[Identity.Api]
Store[Store.Api]
Medicine[Medicine.Api]
end
subgraph Databases
DBIdentity[(identity)]
DBStore[(store)]
DBMedicine[(medicine)]
end
Browser --> Portal
Desktop -->|SignalR| Identity
Desktop -->|SignalR| Store
StoreFE -->|HTTP| YARP
Portal -->|HTTP| YARP
YARP -->|"/Identity/*"| Identity
YARP -->|"/Store/*"| Store
YARP -->|"/Medicine/*"| Medicine
Store -.->|direct HTTP| Identity
Identity --> DBIdentity
Store --> DBStore
Medicine --> DBMedicine
| Service | Type | Purpose |
|---|---|---|
| Mercury.Gateway | YARP reverse proxy | Routes external requests to backend APIs by path prefix. Strips internal auth headers. Rate-limits external traffic. |
| Portal.Web | Blazor Server | Admin panel for pharmacy management. Makes API calls through the gateway. |
| Identity.Api | REST API + SignalR | User authentication, password management, SMS sending. Hosts IdentityHub for desktop clients. |
| Store.Api | REST API + SignalR | E-commerce, orders, products, customers, payments. Hosts StoreHub for real-time support tasks. |
| Medicine.Api | REST API | Drug databases, cross-sale, prescription management, patient SMS. |
| Mercury.Core | Shared library | Contains interfaces, models, and settings shared across all backend services. Not a running service. |
| Mercury Store | Next.js frontend | Customer-facing pharmacy storefront. Separate repository. Calls the gateway over HTTP. |
Communication patterns¶
Mercury uses three communication patterns:
flowchart LR
subgraph "Pattern 1: Gateway-routed HTTP"
C1[Client] -->|HTTP| GW1[Gateway] -->|HTTP| API1[Backend API]
end
flowchart LR
subgraph "Pattern 2: Direct internal HTTP"
S1[Store.Api] -->|"HTTP + X-Internal-Key"| S2[Identity.Api]
end
flowchart LR
subgraph "Pattern 3: SignalR WebSocket"
D1[Desktop Client] <-->|WebSocket| H1[Hub on Backend API]
end
Pattern 1: Gateway-routed HTTP¶
All external-facing traffic goes through the YARP gateway. The gateway matches the URL path prefix, strips it, and forwards the request to the correct backend service.
Browser → GET /Store/api/v1/products
Gateway → strips "/Store" prefix → GET /api/v1/products → Store.Api
Portal.Web and Mercury Store (Next.js) both use this pattern. They never call backend APIs directly — all requests go through the gateway.
Pattern 2: Direct internal HTTP¶
Services inside the cluster can call each other directly by container hostname, bypassing the gateway. These calls include the X-Internal-Key header for authentication.
The gateway strips X-Internal-Key from all inbound requests, so external callers can never reach internal-only endpoints. See Internal API Authentication for details.
Pattern 3: SignalR WebSocket¶
Desktop Medicine clients maintain persistent WebSocket connections to SignalR hubs on backend services. These connections enable real-time RPC and push notifications.
| Hub | Host | Path | Purpose |
|---|---|---|---|
| IdentityHub | Identity.Api | /hubs |
Desktop customer creation, IP-based login, user existence checks |
| StoreHub | Store.Api | /hubs |
Real-time support task notifications to Portal.Web |
SignalR hub URLs are configured in Mercury.Core/Settings/sharedsettings.json:
{
"MercurySettings": {
"Signalr": {
"Identity": "http://api-identity:8080/hubs",
"Store": "http://api-store:8080/hubs",
"Medicine": "http://api-medicine:8080/hubs"
}
}
}
Gateway routing¶
The YARP gateway routes requests based on path prefix. Each route strips the prefix, removes the X-Internal-Key header, and forwards to the destination cluster.
| Route | Path match | Prefix stripped | Destination |
|---|---|---|---|
api-medicine |
/Medicine/{**catch-all} |
/Medicine |
http://api-medicine:8080 |
api-store |
/Store/{**catch-all} |
/Store |
http://api-store:8080 |
api-identity |
/Identity/{**catch-all} |
/Identity |
http://api-identity:8080 |
Each route applies the same transforms:
{
"Transforms": [
{ "PathRemovePrefix": "/Store" },
{ "ResponseHeader": "Source", "Append": "YARP" },
{ "RequestHeaderRemove": "X-Internal-Key" }
]
}
The gateway also enforces a global rate limit: 100 requests per 10-second window per IP address. Exceeding this returns HTTP 429.
Warning
When adding a new route to the gateway, always include RequestHeaderRemove: X-Internal-Key in the transforms. Forgetting this exposes internal endpoints to external callers.
Development vs production destinations¶
The gateway selects the config file at compile time:
#if DEBUG
builder.Configuration.AddJsonFile("yarp.Development.json");
#else
builder.Configuration.AddJsonFile("yarp.json");
#endif
Service-to-service call map¶
This diagram shows every HTTP and SignalR dependency between services.
flowchart TB
Portal[Portal.Web] -->|"HTTP via Gateway<br/>/Identity/*, /Store/*"| GW[Gateway]
StoreFE[Mercury Store<br/>Next.js] -->|"HTTP via Gateway<br/>/Store/*, /Medicine/*, /Identity/*"| GW
GW --> Identity[Identity.Api]
GW --> Store[Store.Api]
GW --> Medicine[Medicine.Api]
Store -->|"direct HTTP<br/>X-Internal-Key<br/>POST /api/v1/sms/send"| Identity
Store -->|"SignalR client<br/>IdentityHubClient"| Identity
Medicine -->|"HTTP via Gateway<br/>/Store/api/v1/store/customer/find-by-phone"| GW
Identity -->|"HTTP<br/>XML/HTTPS"| SMS[Mutlucell SMS Gateway]
Call details¶
| Caller | Target | Method | Path | Purpose |
|---|---|---|---|---|
| Portal.Web | Gateway → Identity | HTTP GET | /Identity/api/v1/AccountCheck/get-identity-user/{guid} |
Check user identity |
| Portal.Web | Gateway → Store | HTTP GET | /Store/api/v1/staff-operation-history/logs |
Fetch staff activity logs |
| Store.Api | Identity.Api (direct) | HTTP POST | http://api-identity/api/v1/sms/send |
Send SMS with X-Internal-Key |
| Store.Api | Identity.Api (direct) | SignalR | http://api-identity:8080/hubs |
Create customers, IP login |
| Store.Api | Gateway → Identity | HTTP POST | /Identity/api/v1/SignIn |
Customer sign-in |
| Medicine.Api | Gateway → Store | HTTP GET | /Store/api/v1/store/customer/find-by-phone |
Verify phone exists before SMS |
| Identity.Api | Mutlucell | HTTP POST | https://smsgw.mutlucell.com/smsgw-ws/sndblkex |
Send SMS (external provider) |
| Mercury Store | Gateway → Store | HTTP | /Store/api/v*/* |
Products, orders, addresses, payments |
| Mercury Store | Gateway → Medicine | HTTP POST | /Medicine/api/v1/store-sms/* |
SMS verification for registration |
| Mercury Store | Gateway → Identity | HTTP PUT | /Identity/api/v1/ChangePassword |
Password changes |
Database ownership¶
Each backend service owns a dedicated PostgreSQL database. Services never share databases or access another service's tables directly.
erDiagram
IdentityApi ||--|| IdentityDB : owns
StoreApi ||--|| StoreDB : owns
MedicineApi ||--|| MedicineDB : owns
IdentityDB {
table Users
table PasswordChangeLinks
table Seeds
table SmsLogs
}
StoreDB {
table Products
table Categories
table Orders
table Customers
table Staffs
table Addresses
table Coupons
table Printers
table Invoices
table Payments
}
MedicineDB {
table Devices
table DevicePairs
table PatientPhones
table PatientPrescriptions
table Drugs
table CrossSales
table SmsLogs
}
| Database | Service | Connection string | Key tables |
|---|---|---|---|
identity |
Identity.Api | cs-identity |
Users, PasswordChangeLinks, SmsLogs |
store |
Store.Api | cs-store |
Products, Orders, Customers, Staffs, Addresses, Printers, Invoices |
medicine |
Medicine.Api | cs-medicine |
Devices, DevicePairs, Drugs, CrossSales, PatientPrescriptions |
Store.Api also uses Hangfire for background jobs, backed by the same store database.
Note
Cross-service data access always happens through HTTP calls, never through shared database connections. If Store.Api needs identity data, it calls Identity.Api.
SignalR hubs¶
IdentityHub¶
Hosted by Identity.Api at /hubs. Store.Api connects as a client using IdentityHubClient (a persistent HubConnection).
| Method | Direction | Purpose |
|---|---|---|
CreateCustomerAsync |
Client → Server | Create a new customer account |
LoginWithIpAddress |
Client → Server | Authenticate by IP address |
IsUserExistsAsync |
Client → Server | Check if a user GUID exists |
StoreHub¶
Hosted by Store.Api at /hubs. Portal.Web receives real-time notifications through this hub.
| Method | Direction | Purpose |
|---|---|---|
ReceiveNewSupportTaskAsync |
Server → Client | New support task created |
ReceiveDoSupportTaskAsync |
Server → Client | Support task claimed |
ReceiveLeaveSupportTaskAsync |
Server → Client | Support task released |
ReceiveRemoveSupportTaskAsync |
Server → Client | Support task removed |
ReceiveMessageAsync |
Server → Client | Chat message in support task |
StoreHub tracks connected clients in a ConcurrentDictionary and pushes events to Portal.Web as they happen.
Relay (planned)¶
The IRelayHub interface is defined in Mercury.Core but not yet implemented. See Mercury Relay for the full design.
Aspire orchestration¶
The development environment is orchestrated by .NET Aspire. The AppHost project wires all services together with service discovery and shared configuration.
graph TB
AppHost[AspireApp.AppHost] -->|provisions| DBIdentity[(db-identity<br/>PostgreSQL)]
AppHost -->|provisions| DBStore[(db-store<br/>PostgreSQL)]
AppHost -->|provisions| DBMedicine[(db-medicine<br/>PostgreSQL)]
AppHost -->|launches| Identity[api-identity]
AppHost -->|launches| Store[api-store]
AppHost -->|launches| Medicine[api-medicine]
AppHost -->|launches| Gateway[gateway]
AppHost -->|launches| Portal[web-portal]
Identity -->|WithReference| DBIdentity
Store -->|WithReference| DBStore
Store -->|WithReference| Identity
Store -->|WithReference| Gateway
Medicine -->|WithReference| DBMedicine
Medicine -->|WithReference| Identity
Medicine -->|WithReference| Gateway
Portal -->|WithReference| Gateway
Portal -->|WithReference| Identity
Portal -->|WithReference| Store
Aspire injects:
- Connection strings for each database (
cs-identity,cs-store,cs-medicine) - Service discovery URLs via
WithReference()so services resolve each other by name - Shared secrets like
InternalApi__KeyviaWithEnvironment()
All three backend APIs receive the same InternalApi__Key value so they can authenticate each other's internal requests.
Service ports¶
| Service | Dev port | Container hostname | Container port |
|---|---|---|---|
| Mercury.Gateway | :5082 |
— | — |
| Identity.Api | :5193 |
api-identity |
:8080 |
| Store.Api | :5141 |
api-store |
:8080 |
| Medicine.Api | :5100 |
api-medicine |
:8080 |
| Portal.Web | :5132 |
web-portal |
— |
| Mercury Store (Next.js) | :3000 |
front_store |
:3000 |
Request flow examples¶
External client fetching products¶
sequenceDiagram
participant Client as Mercury Store (Next.js)
participant GW as Gateway
participant Store as Store.Api
participant DB as store DB
Client->>GW: GET /Store/api/v2/store/allproducts
GW->>GW: Strip /Store prefix
GW->>GW: Strip X-Internal-Key header
GW->>Store: GET /api/v2/store/allproducts
Store->>DB: Query products
DB-->>Store: Product rows
Store-->>GW: 200 OK (JSON)
GW-->>Client: 200 OK (JSON)
Store sending SMS through Identity (internal)¶
sequenceDiagram
participant Store as Store.Api
participant Identity as Identity.Api
participant SMS as Mutlucell SMS
Store->>Identity: POST http://api-identity/api/v1/sms/send
Note over Store,Identity: X-Internal-Key: <shared secret><br/>Direct call, bypasses gateway
Identity->>Identity: Validate X-Internal-Key
Identity->>SMS: POST https://smsgw.mutlucell.com/... (XML)
SMS-->>Identity: SMS sent
Identity-->>Store: 200 OK
Medicine verifying a phone number through Store (cross-service via gateway)¶
sequenceDiagram
participant Medicine as Medicine.Api
participant GW as Gateway
participant Store as Store.Api
Medicine->>GW: GET /Store/api/v1/store/customer/find-by-phone?phone=...
GW->>Store: GET /api/v1/store/customer/find-by-phone?phone=...
Store-->>GW: 200 OK (customer data)
GW-->>Medicine: 200 OK
Source files¶
| File | Purpose |
|---|---|
Mercury.Gateway/yarp.json |
Production YARP route and cluster config |
Mercury.Gateway/yarp.Development.json |
Development YARP config (localhost ports) |
Mercury.Gateway/GatewayProgram.cs |
Gateway startup, config file selection, rate limiting |
Mercury.Core/Identity/RequiresInternalKeyAttribute.cs |
Authorization filter for internal endpoints |
Mercury.Core/Identity/InternalApiOptions.cs |
Config model for InternalApi section |
Mercury.Core/Settings/sharedsettings.json |
Shared settings including SignalR hub URLs |
Mercury.Store.Api/Tools/GatewayClient.cs |
Typed HTTP client for internal and gateway calls |
Mercury.Identity.Api/Signalr/Hubs/IdentityHub.cs |
SignalR hub for desktop client operations |
Mercury.Store.Api/Signalr/Hubs/StoreHub.cs |
SignalR hub for real-time support tasks |
AspireApp.AppHost/AspireProgram.cs |
Aspire orchestration and service wiring |