Purpose: Authoritative record of all integration touch-points between this repo and external systems (firmware, mobile app, portals, Keycloak, Stripe, Firebase FCM, RabbitMQ).
Last scanned: 2026-03-21
Counterpart repos: flower-shop-firmware (ESP32), flower-shop-mobile (Flutter/React Native), portal front-ends (served from this repo), keycloak-config
Table of Contents
- MQTT Topics
- REST API Endpoints
- SignalR Hub — BouquetHub
- Domain / Integration Events (Event Bus)
- Keycloak / JWT Claims & Auth Policies
- Shared Config Keys & Environment Variables
- Firebase FCM Push Notifications
- Portal API Clients (Inbound REST)
1. MQTT Topics
Broker: Mosquitto — configured via MQTT__BrokerHost / MQTT__BrokerPort
Counterpart repo: flower-shop-firmware (ESP32 smart vase firmware)
QoS default is AtLeastOnce (1) unless noted. Retain flag is false unless noted.
1a. Backend → Device (Publish)
| Topic |
Direction |
QoS |
Retain |
Payload Schema |
Owner |
Status |
File |
smartvase/{vendorId}/{deviceId}/commands |
Publish |
1 |
false |
{ "command": string, "timestamp": long, "correlation_id"?: string } |
This repo |
Active |
Infrastructure/Services/MqttService.cs |
smartvase/{vendorId}/{deviceId}/display |
Publish |
1 |
false |
{ "command": "updateDisplay", "timestamp": long, "payload": { "status": string, "price": decimal, "currency": string, "bouquetId": string, "qrCode": string, "message": string } } |
This repo |
Active |
Infrastructure/Services/MqttService.cs |
smartvase/{vendorId}/{deviceId}/wifi |
Publish |
TBD |
false |
{ "ssid": string, "password": string, "securityType": string, "isHidden": bool, "priority": int } |
This repo |
STUB — not implemented |
Application/Services/IMqttService.cs |
smartvase/{vendorId}/{deviceId}/config |
Publish |
1 |
false |
{ "sensor_interval": ulong, "heartbeat_interval": ulong } |
This repo |
DISABLED — firmware ESP32 const-variable bug |
Infrastructure/IoT/MqttDeviceCommunicationService.cs |
devices/{deviceId}/display |
Publish |
1 |
false |
{ "status": string, "price": decimal, "qr_code": string, "freshness_score": int, "custom_message": string, "theme": string, "timestamp": long } |
This repo |
Active (legacy format) |
Infrastructure/IoT/MqttDeviceCommunicationService.cs |
devices/{deviceId}/alerts |
Publish |
1 |
false |
{ "type": string, "severity": string, "message": string, "timestamp": long } |
This repo |
Active |
Infrastructure/IoT/MqttDeviceCommunicationService.cs |
devices/{deviceId}/content |
Publish |
1 |
false |
{ "status": string, "price": decimal, "qr_code_data": string, "freshness_score": int, "custom_message": string, "theme": string, "timestamp": long } |
This repo |
Active |
Infrastructure/IoT/MqttDeviceCommunicationService.cs |
status enum values: available, reserved, sold, removed
theme enum values: default, elegant, modern, rustic
type (alert) enum values: low_battery, connection_issue, sensor_malfunction, maintenance_required
severity enum values: low, medium, high, critical
1b. Device → Backend (Subscribe)
| Topic Pattern |
Direction |
QoS |
Payload Schema |
Owner |
Status |
Handler |
smartvase/platform/heartbeat |
Subscribe |
1 |
See heartbeat schema below |
Firmware |
Active |
MqttDeviceCommunicationService.HandleHeartbeat() |
smartvase/+/+/heartbeat |
Subscribe |
1 |
See heartbeat schema below |
Firmware |
Active |
MqttDeviceCommunicationService.HandleHeartbeat() |
smartvase/+/+/status |
Subscribe |
1 |
{ "device_id": string, "state": string, ... } |
Firmware |
Active |
MqttDeviceCommunicationService |
smartvase/+/+/sensors |
Subscribe |
1 |
See sensor schema below |
Firmware |
Active |
MqttDeviceCommunicationService.HandleSensorData() |
devices/+/heartbeat |
Subscribe |
1 |
Legacy heartbeat |
Firmware |
Active (legacy compat) |
MqttDeviceCommunicationService |
devices/+/status |
Subscribe |
1 |
Legacy status |
Firmware |
Active (legacy compat) |
MqttDeviceCommunicationService |
devices/+/bouquet_detected |
Subscribe |
1 |
{ "device_id": string, ... } |
Firmware |
STUB handler |
MqttDeviceCommunicationService.HandleBouquetDetected() |
devices/+/bouquet_removed |
Subscribe |
1 |
{ "device_id": string, ... } |
Firmware |
STUB handler |
MqttDeviceCommunicationService.HandleBouquetRemoved() |
Heartbeat payload schema (Device → Backend):
{
"device_id": "vase-001",
"shop_id": "shop-42",
"timestamp": 1234567890,
"state": "online|offline",
"wifi_rssi": -65,
"free_heap": 45000,
"uptime": 3600000,
"bouquet_present": true,
"bouquet_price": 89.99,
"temperature": 22.5,
"humidity": 65.0,
"sensor_errors": []
}
Sensor data payload schema (Device → Backend):
{
"temperature": 22.5,
"humidity": 65.0,
"distance": 25.0,
"bouquet_present": true,
"state": "active|idle",
"bouquet_price": 89.99
}
2. REST API Endpoints
Base URL: http://localhost:8080 (dev) — https://api.flowershop.io (prod)
API version prefix: /api/v1/
Counterpart repos: flower-shop-mobile (mobile app), portals (served from this repo), firmware heartbeat endpoint
2a. Public / Anonymous Endpoints
| Method |
Path |
Auth |
Request Body |
Response |
Counterpart |
| POST |
/api/v1/auth/dev-token |
None |
{ username, vendorId?, vendorType?, role? } |
{ token, tokenType, expiresIn, username, vendorId?, vendorType? } |
Dev tooling |
| POST |
/api/v1/admin-accounts/register |
None |
{ username, email, password, firstName?, lastName?, masterKey? } |
{ userId, username, email, role, token, expiresIn, message } |
Admin bootstrap |
| POST |
/api/v1/vendor-accounts/register-with-admin |
None |
{ vendorType, businessName, description?, contactEmail, contactPhone?, latitude, longitude, fullAddress?, adminUsername, adminPassword } |
{ vendorId, vendorType, adminUsername, token, expiresIn } |
Admin Portal |
| POST |
/api/v1/customers/register |
None |
{ username, email, password, customerName, preferredVendorType? } |
{ token, tokenType, expiresIn, username } |
Mobile App |
| GET |
/api/v1/bouquets/nearby |
None |
Query: lat, lng, radiusKm, search?, vendorType?, flowerType?, maxPrice?, sortBy, page, pageSize |
NearbyBouquetsResult |
Mobile App |
| GET |
/api/v1/bouquets/{id} |
None |
— |
BouquetDetailDto |
Mobile App, Portals |
| GET |
/api/v1/bouquets/qr/{code} |
None |
Query: lat?, lng? |
BouquetDetailDto |
Mobile App (QR scan) |
| GET |
/api/v1/bouquets/available |
None |
Query: page, pageSize, category?, priceRange? |
IEnumerable<AvailableBouquetReadModel> |
Customer App |
| GET |
/api/v1/vendors/{id}/public |
None |
— |
PublicVendorDto |
Mobile App |
| GET |
/api/v1/vendors/{id}/bouquets |
None |
Query: page, pageSize |
VendorPublicBouquetsResult |
Mobile App |
| POST |
/api/v1/smartvases/heartbeat/{deviceId} |
None |
{ temperature, humidity, batteryLevel, sensorErrors[]? } |
HeartbeatResponseDto |
Firmware |
| POST |
/api/mock-vase/{vaseId}/bouquet-image |
None |
IFormFile (multipart) |
{ imageUrl } |
Admin Portal, Vendor Portal |
| GET |
/api/mock-vase/image/{vaseId} |
None |
— |
JPEG |
Portals |
| GET |
/api/mock-vase/{vaseId}/status |
None |
— |
VaseStatusResponse |
Dev tooling |
| GET |
/api/v1/reference/flower-types |
None |
— |
List<{ key, label }> |
Mobile App |
| GET |
/api/v1/reference/freshness-labels |
None |
— |
List<{ minScore, maxScore, label, colour }> |
Mobile App |
| GET |
/api/v1/config |
None |
— |
{ stripePublishableKey, supportEmail, termsUrl, privacyPolicyUrl, minAppVersion, forceUpdate } |
Mobile App |
| POST |
/api/v1/vendor-accounts/accept-invitation/{token} |
None |
{ temporaryPassword, newPassword, acceptTerms, deviceInfo? } |
InvitationAcceptanceResult |
Email link → Mobile/Browser |
2b. Customer Endpoints (Policy: CustomerAccess)
| Method |
Path |
Request Body |
Response |
Counterpart |
| GET |
/api/v1/cart |
— |
CartDto |
Mobile App |
| POST |
/api/v1/cart/items |
{ bouquetId } |
CartDto |
Mobile App |
| DELETE |
/api/v1/cart/items/{bouquetId} |
— |
204 |
Mobile App |
| DELETE |
/api/v1/cart |
— |
204 |
Mobile App |
| POST |
/api/v1/orders/preview |
{ bouquetIds[], deliveryMethod, street?, city?, postalCode?, notes? } |
OrderPreviewDto |
Mobile App |
| POST |
/api/v1/orders |
{ bouquetIds[], deliveryMethod, expectedTotalAmount, street?, city?, postalCode?, notes? } + Idempotency-Key header |
OrderCreatedDto |
Mobile App |
| POST |
/api/v1/orders/{id}/confirm-payment |
{ paymentIntentId, stripeCustomerId? } |
204 |
Mobile App (Stripe) |
| POST |
/api/v1/orders/{id}/cancel |
{ reason } |
204 |
Mobile App |
| GET |
/api/v1/orders |
Query: status?, page, pageSize |
OrderListResult |
Mobile App |
| GET |
/api/v1/orders/{id} |
— |
OrderDetailDto |
Mobile App |
| GET |
/api/v1/favourites |
Query: page, pageSize |
FavouritesResult |
Mobile App |
| POST |
/api/v1/favourites/{bouquetId} |
— |
204 |
Mobile App |
| DELETE |
/api/v1/favourites/{bouquetId} |
— |
204 |
Mobile App |
| GET |
/api/v1/subscriptions |
Query: page, pageSize |
SubscriptionListResult |
Mobile App |
| GET |
/api/v1/subscriptions/{id} |
— |
SubscriptionDto |
Mobile App |
| POST |
/api/v1/subscriptions |
{ type, frequency, flowerType?, maxPrice?, currency?, shopId?, centerLatitude?, centerLongitude?, radiusKm? } |
SubscriptionDto |
Mobile App |
| PUT |
/api/v1/subscriptions/{id} |
{ type?, frequency?, flowerType?, maxPrice?, currency?, shopId?, centerLatitude?, centerLongitude?, radiusKm?, isActive? } |
204 |
Mobile App |
| PUT |
/api/v1/subscriptions/disable-all |
— |
204 |
Mobile App |
| DELETE |
/api/v1/subscriptions/{id} |
— |
204 |
Mobile App |
| GET |
/api/v1/notifications |
Query: unreadOnly, page, pageSize |
NotificationListResult |
Mobile App |
| PUT |
/api/v1/notifications/{id}/read |
— |
204 |
Mobile App |
| PUT |
/api/v1/notifications/read-all |
— |
204 |
Mobile App |
| POST |
/api/v1/notifications/devices |
{ deviceToken, platform, appVersion? } |
204 |
Mobile App |
| DELETE |
/api/v1/notifications/devices/{deviceToken} |
— |
204 |
Mobile App |
| GET |
/api/v1/profile |
— |
CustomerProfileDto |
Mobile App |
| PUT |
/api/v1/profile |
{ name?, phoneNumber?, preferredFlowerTypes[]? } |
204 |
Mobile App |
| PUT |
/api/v1/profile/notification-settings |
{ pushEnabled, emailEnabled, subscriptionDigestEnabled } |
204 |
Mobile App |
| DELETE |
/api/v1/profile |
— |
204 |
Mobile App |
| GET |
/api/v1/profile/addresses |
— |
List<SavedAddressDto> |
Mobile App |
| POST |
/api/v1/profile/addresses |
{ label, street, city, postalCode, notes?, isDefault } |
SavedAddressDto |
Mobile App |
| PUT |
/api/v1/profile/addresses/{id} |
{ label?, street?, city?, postalCode?, notes?, isDefault? } |
204 |
Mobile App |
| DELETE |
/api/v1/profile/addresses/{id} |
— |
204 |
Mobile App |
2c. Vendor Endpoints (Policy: VendorAccess)
| Method |
Path |
Request Body |
Response |
Counterpart |
| POST |
/api/v1/bouquets |
CreateBouquetCommand (name, description, price, currency, vaseId?, imageUrls[], flowerTypes[], …) |
BouquetDetails 201 |
Vendor Portal |
| GET |
/api/v1/vendors/{vendorId}/bouquets |
Query: status? |
List<BouquetDetails> |
Vendor Portal |
| PUT |
/api/v1/bouquets/{id} |
UpdateBouquetCommand |
BouquetDetails |
Vendor Portal |
| DELETE |
/api/v1/bouquets/{id} |
— |
204 |
Vendor Portal |
| PUT |
/api/v1/bouquets/{id}/price |
{ price, currency, notes? } |
BouquetDetails |
Vendor Portal |
| GET |
/api/v1/bouquets/{id}/analytics |
Query: period |
BouquetAnalyticsDto |
Vendor Portal |
| GET |
/api/v1/smartvases/my-vases |
— |
List<VaseDetailsDto> |
Vendor Portal |
| GET |
/api/v1/smartvases/{id} |
— |
VaseDetailsDto |
Vendor Portal |
| PATCH |
/api/v1/smartvases/{id}/status |
{ status, price?, qrCodeData?, freshnessScore?, customMessage? } |
200 |
Vendor Portal |
| GET |
/api/v1/analytics/vendor |
Query: period, includeComparisons |
VendorAnalyticsDto |
Vendor Portal — BUG: returns random VendorId data |
| GET |
/api/v1/analytics/vases |
Query: vaseId?, period, metrics? |
IEnumerable<VasePerformanceMetrics> |
Vendor Portal — BUG: returns random VendorId data |
| GET |
/api/v1/analytics/realtime |
Query: refreshRate |
RealtimeAnalyticsDto |
Vendor Portal — BUG: returns random VendorId data |
| GET |
/api/v1/analytics/export |
Query: exportType, format, period |
File (CSV/XLSX) |
Vendor Portal — BUG: returns random VendorId data |
| GET |
/api/v1/analytics/benchmarks |
Query: benchmarkType, vendorType?, period |
BenchmarkAnalyticsDto |
Vendor Portal |
| GET |
/api/v1/vendors/{vendorId}/employees |
Query: includeInactive |
List<EmployeeDto> |
Vendor Portal |
| POST |
/api/v1/vendors/{vendorId}/employees |
{ fullName, email, phone?, role, permissions[]? } |
EmployeeInvitationResult 201 |
Vendor Portal |
| GET |
/api/v1/vendors/{vendorId}/employees/{employeeId} |
— |
EmployeeDetailsDto |
Vendor Portal |
| PUT |
/api/v1/vendors/{vendorId}/employees/{employeeId}/role |
{ newRole, additionalPermissions[]? } |
200 |
Vendor Portal |
| DELETE |
/api/v1/vendors/{vendorId}/employees/{employeeId} |
{ reason, notifyEmployee } |
200 |
Vendor Portal |
| GET |
/api/v1/vendors/{vendorId}/employees/{employeeId}/activity |
Query: pageNumber, pageSize |
PagedResult<EmployeeActivityDto> |
Vendor Portal |
| GET |
/api/v1/vendor-accounts/debug/token-claims |
— |
Token claims |
Dev tooling |
| Method |
Path |
Request Body |
Response |
Counterpart |
| GET |
/api/v1/admin-accounts/me |
— |
AdminInfoResponse |
Admin Portal |
| POST |
/api/v1/admin/vendors |
CreateVendorAccountRequest |
VendorRegistrationResultDto 201 |
Admin Portal |
| GET |
/api/v1/admin/vendors/pending-installation |
— |
List<VendorPendingInstallationDto> |
Admin Portal |
| POST |
/api/v1/admin/vendors/{vendorId}/assign-technician |
{ technicianId, scheduledDate, estimatedDuration, specialInstructions? } |
TechnicianAssignmentResult |
Admin Portal |
| PATCH |
/api/v1/admin/vendors/{vendorId}/status |
{ newStatus, reason, notifyVendor } |
200 |
Admin Portal |
| GET |
/api/v1/admin/analytics |
Query: fromDate?, toDate?, includeVendorBreakdown, includeLocationAnalysis |
PlatformAnalyticsDto |
Admin Portal |
| POST |
/api/v1/admin/reports/vendor-onboarding |
VendorOnboardingReportRequest |
VendorOnboardingReportDto |
Admin Portal |
| POST |
/api/v1/smartvases |
{ deviceId, latitude, longitude, locationName?, vendorId?, vendorType? } |
VaseRegistrationResult 201 |
Admin Portal |
| GET |
/api/v1/smartvases |
— |
List<VaseDetailsDto> |
Admin Portal |
| GET |
/api/v1/smartvases/check-device/{deviceId} |
— |
bool |
Admin Portal |
| POST |
/api/v1/smartvases/{id}/migrate |
{ targetVendorId, targetVendorType, reason } |
VaseMigrationResult 202 |
Admin Portal |
| POST |
/api/v1/smartvases/{id}/ping |
— |
PingVaseResult |
Admin Portal |
| POST |
/api/v1/smartvases/{id}/restart |
— |
VaseCommandResult |
Admin Portal |
| POST |
/api/v1/smartvases/{id}/configure |
{ brightness?, contrast?, sensorSensitivity?, privacyMode? } |
VaseCommandResult |
Admin Portal |
| POST |
/api/v1/smartvases/{id}/unassign |
— |
VaseCommandResult |
Admin Portal |
| GET |
/api/v1/analytics/platform |
Query: period, vendorType?, region? |
PlatformInsightsDto |
Admin Portal |
| POST |
/api/mock-vase/{vaseId}/ping |
— |
PingResponse |
Admin Portal |
2e. Common Response Errors
| HTTP Status |
Meaning |
Common Error Strings |
| 400 |
Validation / business rule error |
vendor-mismatch, price-changed, bouquet-not-available |
| 401 |
Unauthenticated |
— |
| 403 |
Wrong tenant / insufficient role |
— |
| 404 |
Resource not found |
— |
| 409 |
Conflict (idempotency / concurrent update) |
price-changed, vendor-mismatch |
| 410 |
Gone (bouquet sold/expired) |
— |
3. SignalR Hub — BouquetHub
Hub URL: /hubs/bouquet
Transport: WebSocket with exponential backoff reconnect (0s, 2s, 5s, 10s, 30s)
Counterpart repos: flower-shop-mobile, portals (Admin/Vendor/Customer)
3a. Client → Server Methods
| Method |
Auth Required |
Parameters |
Group Joined |
Purpose |
JoinRadiusGroup |
No |
lat: double, lng: double, radiusKm: double |
radius-{lat:0.00}-{lng:0.00}-{radiusKm} |
Subscribe to geo-radius bouquet events |
LeaveRadiusGroup |
No |
lat: double, lng: double, radiusKm: double |
— |
Unsubscribe from radius group |
JoinOrderGroup |
Yes |
orderId: string |
order-{orderId} |
Subscribe to order status updates |
LeaveOrderGroup |
Yes |
orderId: string |
— |
Unsubscribe from order group |
JoinVendorGroup |
Internal check |
vendorId: string, vendorType: string |
vendor-{vendorId}, vendortype-{vendorType} |
Vendor portal subscription |
JoinCustomerGroup |
No |
customerId: string |
customer-{customerId}, customers |
Customer portal subscription |
StartViewingBouquet |
No |
bouquetId: string |
— |
Increment real-time view counter |
StopViewingBouquet |
No |
bouquetId: string |
— |
Decrement real-time view counter |
3b. Server → Client Events
| Event Name |
Target Group(s) |
Payload Schema |
Status |
Trigger |
BouquetBecameAvailable |
customers (all), radius groups |
{ bouquetId, vendorId, vendorType, status, price, currency, imageUrl, timestamp } |
Active |
Bouquet created/released from cart |
BouquetBecameUnavailable |
customers (all) |
{ bouquetId, vendorType, reason, timestamp } |
Active |
Sold / reserved / vase offline |
BouquetPriceChanged |
customers, vendor-{vendorId} |
{ bouquetId, price, currency, timestamp } |
Active |
Vendor updates price |
BouquetViewCountChanged |
customers |
{ bouquetId, viewCount, timestamp } |
Active |
View counter incremented/decremented |
BouquetStatusChanged |
vendor-{vendorId} |
{ bouquetId, status, vendorType, timestamp } |
Active |
Bouquet status changes |
BouquetDeleted |
vendor-{vendorId}, customers |
{ bouquetId, reason, timestamp } |
Active |
Vendor deletes bouquet |
OrderStatusChanged |
order-{orderId}, customer-{customerId}, vendor-{vendorId} |
{ orderId, status, label, timestamp, estimatedDeliveryAt? } |
Active |
Order lifecycle transitions |
VaseStatusChanged |
vendor-{vendorId} |
{ vaseId, status, timestamp } |
Active |
Vase heartbeat monitor detects change |
FreshnessUpdated |
radius groups |
{ bouquetId, freshnessScore, freshnessLabel, timestamp } |
Defined, NOT yet triggered |
MQTT sensor data → freshness calc |
DeliveryLocationUpdated |
order-{orderId} |
{ orderId, courierLatitude, courierLongitude, estimatedMinutesRemaining, timestamp } |
Not implemented (EP-07) |
Courier location update |
status values for OrderStatusChanged: created, paid, preparing, ready_for_pickup, in_delivery, delivered, cancelled
reason values for BouquetBecameUnavailable: sold, reserved, vase_offline, removed
status values for VaseStatusChanged: online, offline, error
4. Domain / Integration Events (Event Bus)
Current implementation: InMemoryEventBus via MediatR (in-process)
Planned implementation: RabbitMQEventBus — EP-09 (currently stub, events silently dropped)
RabbitMQ connection: amqp://flowershop:dev123@localhost:5672/ (dev)
Counterpart repos: None currently (all in-process); future: analytics service, notification worker
When RabbitMQ is wired, these events will become inter-service contracts.
4a. Bouquet Events
| Event Class |
Key Fields |
Published By |
Consumed By |
BouquetCatalogCreated |
BouquetId, Name, VendorId, VendorType, CreatedAt |
CreateBouquetCommandHandler |
RealTimeNotificationService |
BouquetPlacedInVase |
BouquetId, VaseId, VendorId, VendorType, PlacedAt |
Bouquet aggregate |
— |
BouquetPriceSet |
BouquetId, Price (PriceInfo), SetBy, VendorType, Method (PricingMethod) |
Bouquet aggregate |
MqttService, RealTimeNotificationService |
BouquetPriceUpdated |
BouquetId, PreviousPrice, NewPrice, UpdatedBy, Method, Reason |
UpdateBouquetCommandHandler |
RealTimeNotificationService |
BouquetBecameAvailable |
BouquetId, VaseId, VendorId, VendorType |
Bouquet aggregate |
RealTimeNotificationService |
BouquetReserved |
BouquetId, CustomerId, ReservedAt, ReservationExpires |
Bouquet aggregate |
RealTimeNotificationService |
BouquetSold |
BouquetId, VaseId, VendorId, SoldTo, OrderId, Price, SoldAt |
Bouquet aggregate |
RealTimeNotificationService |
BouquetRemovedFromVase |
BouquetId, VaseId, VendorId, Reason (RemovalReason), RemovedBy, PreviousStatus, RemovedAt |
DeleteBouquetCommandHandler |
RealTimeNotificationService |
4b. Order Events
| Event Class |
Key Fields |
Published By |
Consumed By |
OrderCreatedEvent |
OrderId, CustomerId, BouquetIds[], VendorId, VendorType, TotalAmount |
Order aggregate |
RealTimeNotificationService |
OrderPaidEvent |
OrderId, CustomerId, BouquetIds[], VendorId, PaymentIntentId |
Order aggregate |
RealTimeNotificationService |
OrderCancelledEvent |
OrderId, CustomerId, BouquetIds[], VendorId, PaymentIntentId, PreviousStatus |
Order aggregate |
RealTimeNotificationService |
ReservationExpiredEvent |
OrderId, CustomerId, BouquetIds[] |
Order aggregate |
Bouquet release handler |
4c. SmartVase Events
| Event Class |
Key Fields |
Published By |
Consumed By |
SmartVaseCreated |
VaseId, DeviceId |
SmartVase aggregate |
— |
VaseAssignedToVendor |
VaseId, VendorId, VendorType, Configuration |
SmartVase aggregate |
— |
VaseRegistrationCompletedByTechnician |
VaseId, DeviceId, VendorId, VendorType, TechnicianId, InstallationLocation, CompletedAt, Configuration |
SmartVase aggregate |
— |
VaseHealthStatusChanged |
VaseId, NewStatus (VaseHealthStatus), PreviousStatus, HealthData |
SmartVase aggregate |
RealTimeNotificationService |
VaseWentOffline |
VaseId, VendorId, VendorType, OfflineAt |
VaseHealthMonitoringService |
RealTimeNotificationService |
VaseMigrationInitiated |
Migration (VaseMigration) |
SmartVase aggregate |
— |
VaseMigrationCompleted |
VaseId, PreviousAssignment, NewAssignment |
SmartVase aggregate |
— |
VaseUnassignedFromVendor |
VaseId, PreviousVendorId, PreviousVendorType, UnassignedAt |
SmartVase aggregate |
— |
4d. Vendor Events
| Event Class |
Key Fields |
Published By |
Consumed By |
FlowerShopRegistered |
VendorId, ShopProfile, Location |
FlowerShop aggregate |
VendorEventHandlers → INotificationServiceClient |
FreelanceFloristRegistered |
VendorId, FloristProfile, ResidentialLocation |
FreelanceFlorist aggregate |
VendorEventHandlers → INotificationServiceClient |
VendorRegistered |
VendorId, VendorType, RegisteredAt |
VendorEvents |
— |
VendorProfileCompleted |
VendorId, VendorType, CompletedAt |
VendorEvents |
— |
ShopProfileUpdated |
VendorId, NewProfile, PreviousProfile, UpdatedBy |
FlowerShop aggregate |
— |
ShopLocationUpdated |
VendorId, NewLocation |
FlowerShop aggregate |
— |
FloristPrivacySettingsUpdated |
VendorId, NewSettings, PreviousSettings, UpdatedBy |
FreelanceFlorist aggregate |
— |
FloristGrowthStageAdvanced |
VendorId, Milestone, PreviousStage, RecordedBy |
FreelanceFlorist aggregate |
VendorEventHandlers → INotificationServiceClient |
VendorTypeChanged |
VendorId, FromType, ToType |
Application handlers |
INotificationServiceClient |
VaseAssignedToShop |
VendorId, VaseId, VaseConfiguration |
FlowerShop aggregate |
— |
VaseAssignedToFlorist |
VendorId, VaseId, HomeVaseConfiguration, BusinessGrowthStage |
FreelanceFlorist aggregate |
— |
4e. Employee Events
| Event Class |
Key Fields |
Published By |
Consumed By |
EmployeeInvitationCreated |
EmployeeId, VendorId, FullName, Email, Role, InvitationToken, CreatedBy |
Employee aggregate |
Email service |
EmployeeInvitationAccepted |
EmployeeId, VendorId, AcceptedAt |
Employee aggregate |
Keycloak user creation |
EmployeeRoleUpdated |
EmployeeId, VendorId, PreviousRole, NewRole, UpdatedBy |
Employee aggregate |
— |
EmployeePermissionGranted |
EmployeeId, VendorId, Permission, GrantedBy |
Employee aggregate |
— |
EmployeePermissionRevoked |
EmployeeId, VendorId, Permission, RevokedBy |
Employee aggregate |
— |
EmployeeDeactivated |
EmployeeId, VendorId, DeactivatedBy, Reason |
Employee aggregate |
Keycloak user disable |
5. Keycloak / JWT Claims & Auth Policies
Counterpart repos: keycloak-config (realm configuration), flower-shop-mobile, portals
| Claim Name |
Type |
Source |
Description |
Required By |
sub |
string (UUID) |
Keycloak standard |
Keycloak user ID |
TenantResolutionMiddleware |
ClaimTypes.NameIdentifier |
string |
Standard JWT |
Maps to sub |
Auth handlers |
vendor_id |
string (GUID) |
Keycloak custom |
Vendor’s ID |
TenantResolutionMiddleware → VendorContext |
vendor_type |
string |
Keycloak custom |
TraditionalShop or FreelanceFlorist |
TenantResolutionMiddleware → VendorContext |
customer_id |
string (GUID) |
Keycloak custom |
Customer’s ID |
TenantResolutionMiddleware → CustomerContext |
ClaimTypes.Role |
string |
Keycloak standard |
User role string |
Auth policy checks |
realm_access.roles[] |
string[] |
Keycloak JWT |
Array of realm roles |
Role-based policy evaluation |
5b. Auth Policies
| Policy Name |
Required Claims / Roles |
Used On |
CustomerAccess |
Role must be User + valid customer_id claim |
All /api/v1/cart, /api/v1/orders, /api/v1/profile, /api/v1/favourites, /api/v1/subscriptions, /api/v1/notifications |
VendorAccess |
Role must be VendorAdmin + valid vendor_id + vendor_type claims |
All vendor bouquet / vase / analytics / employee endpoints |
PlatformAdmin |
Role must be PlatformAdmin |
All /api/v1/admin/*, vase registration, platform analytics |
ShopAdmin |
Role must be ShopAdmin (employee sub-role) |
Employee management write operations |
5c. Keycloak Server Configuration (Dev)
| Key |
Value |
| Authority URL |
http://keycloak-dev:8080/realms/flowershop (container) / http://localhost:8090/realms/flowershop (host) |
| Client ID |
flowershop-api |
| Client Secret |
flowershop-secret |
| Admin Realm |
master |
| Admin Username |
admin |
| Admin Password |
admin123 |
| Admin Client ID |
admin-cli |
| Realm Name |
flowershop |
External repos must configure the same realm name, client ID, and custom claim mappers (vendor_id, vendor_type, customer_id) in Keycloak.
6. Shared Config Keys & Environment Variables
Counterpart repos: keycloak-config, flower-shop-firmware, deployment infrastructure
6a. .NET Configuration Keys (IConfiguration paths)
| Config Key |
Type |
Description |
Default (dev) |
ConnectionStrings:DefaultConnection |
string |
Primary PostgreSQL |
Host=postgres-dev;Database=flowershop_iot_dev;Username=postgres;Password=dev123; |
ConnectionStrings:ReadConnection |
string |
Read replica PostgreSQL |
Host=postgres-dev;Database=flowershop_iot_read_dev;... |
ConnectionStrings:Redis |
string |
Redis |
redis-dev:6379 |
JWT:SecretKey |
string |
HMAC signing key (≥32 chars) |
Dev key only |
JWT:Issuer |
string |
JWT issuer claim |
FlowerShopIoTPlatformDev |
JWT:Audience |
string |
JWT audience claim |
FlowerShopIoTUsersDev |
JWT:ExpirationHours |
int |
Token lifetime |
8760 (dev), 24 (prod) |
Authentication:UseKeycloak |
bool |
Enable Keycloak vs dev-token flow |
true |
Authentication:Keycloak:Authority |
string |
Keycloak OIDC authority URL |
See §5c |
Authentication:Keycloak:ClientId |
string |
API client ID |
flowershop-api |
Authentication:Keycloak:ClientSecret |
string |
API client secret |
flowershop-secret |
Authentication:Keycloak:AdminRealm |
string |
Keycloak admin realm |
master |
Authentication:Keycloak:AdminUsername |
string |
Keycloak admin user |
admin |
Authentication:Keycloak:AdminPassword |
string |
Keycloak admin password |
admin123 |
Authentication:Keycloak:AdminClientId |
string |
Admin CLI client |
admin-cli |
MQTT:BrokerHost |
string |
MQTT broker hostname |
mqtt-dev (container) / localhost (host) |
MQTT:BrokerPort |
int |
MQTT broker port |
1883 |
MQTT:Username |
string |
MQTT auth username |
flowershop |
MQTT:Password |
string |
MQTT auth password |
dev123 |
EventBus:Type |
string |
InMemory or RabbitMQ |
InMemory |
EventBus:RabbitMQ:ConnectionString |
string |
RabbitMQ AMQP URL |
amqp://flowershop:dev123@localhost:5672/ |
Features:EnableRealTimeUpdates |
bool |
Enable SignalR hub |
true |
Features:EnableAnalytics |
bool |
Enable analytics handlers |
true |
Features:EnableVaseHealthMonitoring |
bool |
Enable background heartbeat monitor |
false (dev) |
Features:MaxVasesPerVendor |
int |
Business rule limit |
10 |
Features:MaxBouquetsPerVendor |
int |
Business rule limit |
50 |
Cors:AllowedOrigins |
string[] |
CORS origins |
["*"] (dev) |
6b. Docker Compose Environment Variables
| Variable |
Service |
Value (dev) |
POSTGRES_DB |
postgres-dev |
flowershop_iot_dev |
POSTGRES_USER |
postgres-dev |
postgres |
POSTGRES_PASSWORD |
postgres-dev |
dev123 |
RABBITMQ_DEFAULT_USER |
rabbitmq-dev |
flowershop |
RABBITMQ_DEFAULT_PASS |
rabbitmq-dev |
dev123 |
RABBITMQ_DEFAULT_VHOST |
rabbitmq-dev |
/ |
KEYCLOAK_ADMIN |
keycloak-dev |
admin |
KEYCLOAK_ADMIN_PASSWORD |
keycloak-dev |
admin123 |
KC_DB_USERNAME |
keycloak-dev |
postgres |
KC_DB_PASSWORD |
keycloak-dev |
dev123 |
MQTT__BrokerHost |
api-dev |
mqtt-dev |
MQTT__BrokerPort |
api-dev |
1883 |
MQTT__Username |
api-dev |
flowershop |
MQTT__Password |
api-dev |
dev123 |
JWT__SecretKey |
api-dev |
Dev key |
JWT__Issuer |
api-dev |
FlowerShopIoTPlatformDev |
JWT__Audience |
api-dev |
FlowerShopIoTUsersDev |
Authentication__UseKeycloak |
api-dev |
true |
Authentication__Keycloak__Authority |
api-dev |
http://keycloak-dev:8080/realms/flowershop |
Authentication__Keycloak__ClientId |
api-dev |
flowershop-api |
7. Firebase FCM Push Notifications
Interface: INotificationServiceClient
Implementation status: STUB — pending EP-06
Counterpart repo: flower-shop-mobile (receives FCM tokens and push messages)
| Method |
Parameters |
Purpose |
Status |
SendVendorWelcomeNotificationAsync |
vendorId, vendorType, vendorName, cancellationToken |
Welcome email/push on vendor registration |
Stub |
SendGrowthStageAdvancementNotificationAsync |
vendorId, newStage, previousStage, cancellationToken |
Notify florist of growth milestone |
Stub |
SendVendorTypeChangeNotificationAsync |
vendorId, newType, previousType, cancellationToken |
Notify on vendor type transition |
Stub |
SendComplianceAlertAsync |
vendorId, alertType, message, cancellationToken |
Platform compliance alert |
Stub |
SendMaintenanceNotificationAsync |
message, targetVendors?, cancellationToken |
Broadcast maintenance notice |
Stub |
SendBouquetAvailabilityNotificationAsync |
notificationData: object, cancellationToken |
Push to subscribed customers |
Stub |
Device registration endpoint: POST /api/v1/notifications/devices — accepts { deviceToken, platform, appVersion? }
8. Portal API Clients (Inbound REST)
The three portals (served from this repo) consume the REST API above. This section records which endpoints each portal calls, for dependency tracing.
Admin Portal (FlowerShop.AdminPortal)
| Endpoint Called |
Purpose |
POST /api/v1/vendor-accounts/register-with-admin |
Create vendor + admin user |
GET /api/v1/admin/vendors/pending-installation |
List pending vendors |
GET /api/v1/vendors |
All vendors list |
GET /api/v1/vendors/{id} |
Vendor details |
PUT /api/v1/vendors/{id}/location |
Update vendor location |
POST /api/v1/smartvases |
Register new vase |
GET /api/v1/smartvases |
All vases list |
GET /api/v1/smartvases/{id} |
Vase details |
GET /api/v1/smartvases/check-device/{deviceId} |
Device ID check |
POST /api/v1/smartvases/{id}/ping |
Ping vase via MQTT |
POST /api/v1/smartvases/{id}/restart |
Restart vase |
POST /api/v1/smartvases/{id}/configure |
Configure vase |
POST /api/v1/smartvases/{id}/unassign |
Unassign vase |
POST /api/mock-vase/{id}/bouquet-image |
Upload bouquet image (mock endpoint) |
Vendor Portal (FlowerShop.VendorPortal)
| Endpoint Called |
Purpose |
Known Issue |
GET /api/v1/vendors/{id} |
Vendor profile |
VendorId read from session, not JWT |
GET /api/v1/smartvases/my-vases |
Vendor’s vases |
— |
GET /api/v1/smartvases/{id} |
Vase details |
— |
POST /api/v1/bouquets |
Create bouquet |
— |
PUT /api/v1/bouquets/{id} |
Update bouquet |
— |
GET /api/v1/bouquets/{id} |
Get bouquet |
— |
GET /api/v1/vendors/{id}/bouquets |
List vendor bouquets |
— |
DELETE /api/v1/bouquets/{id} |
Delete bouquet |
— |
PUT /api/v1/bouquets/{id}/price |
Set price |
— |
POST /api/mock-vase/{id}/bouquet-image |
Upload photo |
Hardcoded http://localhost:8080 URL |
Customer App (FlowerShop.CustomerApp)
| Endpoint Called |
Purpose |
Known Issue |
GET /api/v1/bouquets/available |
Browse bouquets |
— |
GET /api/v1/bouquets/{id} |
Bouquet detail |
— |
POST /api/v1/cart/{customerId}/items/{bouquetId} |
Add to cart |
Old cart path — mismatches current API |
DELETE /api/v1/cart/{customerId}/items/{bouquetId} |
Remove from cart |
Old cart path |
GET /api/v1/cart/{customerId}/items |
Get cart |
Old cart path |
DELETE /api/v1/cart/{customerId} |
Clear cart |
Old cart path |
GET /api/v1/cart/{customerId}/count |
Cart count |
Old cart path |
Note: Customer App cart calls use legacy path format cart/{customerId}/... which does not match the current API controller route cart/... (tenant from JWT). This is a known EP-13 gap.
Known Stubs & Gaps (Contract Risk)
| Gap |
Risk Level |
Epic |
RabbitMQEventBus.PublishAsync returns Task.CompletedTask — all events silently dropped when RabbitMQ is configured |
High |
EP-09 S-09-06 |
INotificationServiceClient is a no-op stub — no FCM pushes sent |
High |
EP-06 |
AnalyticsController uses VendorId.New() — all analytics return wrong tenant data |
High |
EP-10 T-10-001 |
CacheService uses IMemoryCache not Redis |
Medium |
EP-09 T-09-003 |
IStripeService not wired — CreateOrderCommandHandler never calls Stripe |
High |
EP-04 |
FreshnessUpdated SignalR event defined but never triggered |
Medium |
EP-08 |
DeliveryLocationUpdated SignalR event in API contract but not implemented |
Low |
EP-07 |
MQTT config sending disabled (smartvase/+/+/config) — firmware bug |
Low |
firmware fix needed |
| Customer App portal cart path mismatch vs current API |
Medium |
EP-13 |
| DB migration not run: Cart, CartItem, OrderLines, Subscription, Favourite, Notification, SavedAddress tables missing |
Critical |
EP-01 |