Skip to content

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.

Store.Api → POST http://api-identity/api/v1/sms/send
            Header: X-Internal-Key: <shared secret>

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

api-medicine  → http://api-medicine:8080
api-store     → http://api-store:8080
api-identity  → http://api-identity:8080
api-medicine  → http://localhost:5100
api-store     → http://localhost:5141
api-identity  → http://localhost:5193

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__Key via WithEnvironment()

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