Skip to main content

Domain 10: Integration Architecture

Domain Owner: Backend (ASP.NET Core) / Mobile (Flutter + Dio) / DevOps (Docker + Coolify) Last Updated: 2026-03-10 Workflows: WF-INTEG-01 through WF-INTEG-08

Domain Introduction

The Integration Architecture domain documents every external service that the Almafrica platform connects to, how those connections are configured, and how data flows between the system and third-party providers. This domain serves as the single reference for understanding the platform's external dependencies and their failure modes.

Key architectural principles:

  • Graceful degradation: Every external service integration is designed to fail safely. Missing configuration logs a warning and disables the feature rather than crashing the application.
  • S3-compatible abstraction: File storage uses the AWS SDK's S3 interface against DigitalOcean Spaces, allowing provider portability.
  • Development/production parity: OTP and push notification services support a development mode that logs actions instead of sending real messages, ensuring safe local testing.
  • Offline-resilient mobile: The mobile app detects connectivity with DNS probes and captive portal checks, queues operations when offline, and resumes uploads with chunked/resumable transfers.
  • Container-first deployment: All services are containerized with Docker and orchestrated through Coolify, using environment variable injection for secrets.

External Services Overview


Workflows


WF-INTEG-01: DigitalOcean Spaces — Regular Image Upload

Trigger: Backend receives an image file via API (farmer photo, quality assessment image, client document) Frequency: Multiple times daily per active agent Offline Support: No (requires network; see WF-INTEG-02 for chunked/resumable alternative) Cross-references: WF-FARM-01 (farmer registration photo), WF-QA-01 (assessment photos), WF-CAMP-03 (survey attachments)

Workflow Diagram

Configuration

SettingEnv VariableDefaultDescription
DigitalOceanSpaces:AccessKeyDO_SPACES_ACCESS_KEY(required)S3 access key
DigitalOceanSpaces:SecretKeyDO_SPACES_SECRET_KEY(required)S3 secret key
DigitalOceanSpaces:SpaceNameDO_SPACES_NAME(required)Bucket name
DigitalOceanSpaces:RegionDO_SPACES_REGIONnyc3S3 region
DigitalOceanSpaces:EndpointDO_SPACES_ENDPOINThttps://nyc3.digitaloceanspaces.comS3 endpoint URL
DigitalOceanSpaces:CdnEndpointDO_SPACES_CDN_ENDPOINT(empty)Optional CDN URL prefix

Key Implementation Details

  • S3 Client: Uses AmazonS3Client from the AWS SDK with ForcePathStyle = true (required by DigitalOcean Spaces).
  • Image Processing: Uses SixLabors.ImageSharp for resize and format conversion. Max dimensions: 500x500. JPEG quality: 85.
  • ACL: All uploaded objects are set to S3CannedACL.PublicRead for direct URL access.
  • Unique Naming: Every file gets a GUID-based name to prevent collisions: {folder}/{Guid.NewGuid()}{extension}.
  • Fallback Strategy: If image processing fails (unsupported format, corrupted stream), the service retries with the original raw stream.

Source Files

FilePurpose
backend/Almafrica.Infrastructure/Services/DigitalOceanSpacesService.csS3 upload/delete with image processing
backend/Almafrica.Infrastructure/Services/MediaUploadService.csValidation layer (5MB limit, extension whitelist)
backend/Almafrica.Application/Interfaces/IObjectStorageService.csAbstraction interface

WF-INTEG-02: DigitalOcean Spaces — Chunked/Resumable Upload

Trigger: Mobile app needs to upload a file larger than a single HTTP request can reliably handle, or network conditions are unstable Frequency: On-demand (large documents, batch photo uploads) Offline Support: Partial (upload state persisted to SQLite; resumes when connectivity returns) Cross-references: WF-SYNC-03 (offline queue processing), WF-CLI-02 (client document uploads)

Workflow Diagram

Chunk Configuration

ParameterValueDescription
Default chunk size512 KBMobile-side default per part
Min chunk size100 KBBackend floor clamp
Max chunk size5 MBBackend ceiling clamp
Max file size50 MBTotal file size limit
Part timeout60 secondsPer-chunk upload timeout
Max retries per part3With exponential backoff
Max consecutive failures10Before marking upload as failed
Presigned URL expiry60 minutesConfigurable via PresignedUrlExpirationMinutes
Allowed foldersclients, client-documents, crop-demands, farmers, uploadsSecurity whitelist

Resume Flow

Source Files

FilePurpose
mobile/mon_jardin/lib/data/services/chunked_upload_service.dartMobile chunked upload orchestrator
backend/Almafrica.Infrastructure/Services/MultipartUploadService.csBackend S3 multipart management
backend/Almafrica.Application/Interfaces/IMultipartUploadService.csAbstraction interface

WF-INTEG-03: Africa's Talking SMS/OTP Integration

Trigger: User requests OTP for phone verification (login 2FA, phone number change) Frequency: On-demand (each OTP request) Offline Support: No (requires network) Cross-references: WF-AUTH-01 (login flow), WF-AUTH-04 (two-factor authentication)

Workflow Diagram

OTP Verification Flow

Configuration

SettingEnv VariableDefaultDescription
Otp:DevelopmentModeOTP_DEV_MODEtrue (dev) / false (prod)When true, OTP is logged, not sent
Otp:SmsProviderSMS_PROVIDERAfricasTalkingSMS provider identifier
Otp:ExpiryMinutes10OTP validity window
Otp:MaxAttempts5Max verification attempts per OTP

Current Implementation Status

The OTP service is fully implemented for generation, storage, and verification. The SMS sending integration with Africa's Talking is stubbed with a TODO comment. In development mode, OTPs are logged to the console with the fixed bypass code 000000. In production mode, OTPs are generated but the actual SMS send call is commented out pending provider integration.

Cleanup

The CleanupExpiredOtpsAsync method removes OTP records older than 7 days (used or expired). This can be invoked periodically via a background service or scheduled task.

Source Files

FilePurpose
backend/Almafrica.Infrastructure/Services/OtpService.csOTP generation, verification, and cleanup
backend/Almafrica.Application/Interfaces/IOtpService.csAbstraction interface

WF-INTEG-04: SignalR Real-Time Communication

Trigger: Backend needs to push data to connected mobile/web clients (permission changes, entity updates) Frequency: On-demand (whenever permissions or data change) Offline Support: N/A (WebSocket requires active connection; mobile reconnects automatically) Cross-references: WF-AUTH-05 (permission broadcast), WF-AUTH-06 (permission revocation handling), WF-SYNC-01 (sync triggers)

Hub Architecture

Connection Lifecycle (Mobile)

SignalR Server Configuration

SettingValueDescription
EnableDetailedErrorstrueDetailed error messages in development
KeepAliveInterval15 secondsServer ping interval
ClientTimeoutInterval30 secondsTime before considering client disconnected
Authentication[Authorize(AuthenticationSchemes = "Bearer")]JWT required for hub connection
Hub path/hubs/permissionsWebSocket endpoint
Health checkGET /health/signalrReturns { status: "Healthy", service: "SignalR" }

Message Types

MethodDirectionPayloadPurpose
PermissionsUpdatedServer to User{ userId, permissions[], timestamp }Targeted permission update
DataChangedServer to All{ entityType, entityId, action, timestamp }Broadcast data change notification
GetConnectionIdClient invokeReturns stringClient can query its own connection ID

Mobile Reconnect Strategy

AttemptDelayMax
11 second
22 seconds
34 seconds
48 seconds
516 seconds
6+30 secondsCapped

Source Files

FilePurpose
backend/Almafrica.API/Hubs/PermissionHub.csSignalR hub with JWT auth, connection logging
backend/Almafrica.API/Services/PermissionBroadcastService.csServer-side broadcast logic
backend/Almafrica.Application/Interfaces/IPermissionBroadcastService.csAbstraction interface
mobile/mon_jardin/lib/data/services/permission_sync_service.dartMobile SignalR client
backend/Almafrica.API/Extensions/ServiceCollectionExtensions.csSignalR DI registration
backend/Almafrica.API/Extensions/WebApplicationExtensions.csHub endpoint mapping

WF-INTEG-05: PostgreSQL + EF Core Data Persistence

Trigger: Any API request that reads or writes data Frequency: Every API call Offline Support: N/A (backend requires database connectivity) Cross-references: All backend workflows depend on this integration

Connection Architecture

Connection String Resolution

The backend resolves connection strings in the following priority order:

  1. ConnectionStrings:almafricadb (standard .NET config)
  2. ConnectionStrings__almafricadb (Coolify double-underscore format)
  3. Automatic conversion of postgres:// URIs to Npgsql format (for Coolify-managed PostgreSQL)

EF Core Configuration

SettingValueDescription
ProviderNpgsql (PostgreSQL)Primary database
Dynamic JSONEnabledList<T> serialized as jsonb columns
History table__efmigrations_historySnake_case naming
InterceptorAuditSaveChangesInterceptorImmutable audit trail on every save
NamingSnake_caseCustom SnakeCaseHistoryRepository

Migration Strategy

SettingValueDescription
Auto-migrate on startupConfigurable via RunMigrationsDefault: true
Retry attempts10For container startup race conditions
Retry delay3 secondsBetween attempts
Production overrideRunMigrations: falseDisabled in production docker-compose

WF-INTEG-06: Redis Cache (Currently Disabled)

Trigger: N/A (disabled in current deployment) Frequency: N/A Cross-references: WF-AUTH-01 (potential session caching), WF-SYNC-01 (potential sync state caching)

Current Status

Redis is provisioned in Docker Compose (local profile) but the client registration in the application is commented out with the note: "Redis disabled temporarily - will add back once connection is fixed." The application currently uses IMemoryCache (in-process) and HTTP response caching as alternatives.

Docker Compose Provisioning

EnvironmentImagePortVolumeProfile
Localredis:7-alpine6379redis_dev_datalocal-cache (opt-in)
Dev/StagingCoolify-managedInjected via REDIS_URLManagedAlways available
ProductionCoolify-managedInjected via REDIS_URLManagedAlways available

Intended Architecture (When Re-enabled)

  • Session token blacklist (for logout invalidation)
  • API response caching for master data endpoints
  • Rate limiting counters
  • SignalR backplane for multi-instance scaling

WF-INTEG-07: Firebase Cloud Messaging (Push Notifications)

Trigger: Backend event requiring user notification (stock loss approval, order status change, expiry alert) Frequency: On-demand + scheduled (ExpiryAlertBackgroundService) Offline Support: N/A (server-side; FCM handles device delivery when device comes online) Cross-references: WF-NOTIF-01 (push notification workflow), WF-STOCK-05 (stock loss approval notification)

Workflow Diagram

Device Token Management

Configuration

SettingEnv VariableDefaultDescription
Firebase:ProjectId(empty)GCP project ID
Firebase:CredentialsPath(empty)Path to service account JSON
Firebase:EnabledfalseMaster switch for FCM

Current Status

Firebase is fully coded but disabled (Enabled: false). When disabled, the service logs what it would send and returns Success (no-op). The FCM message payload includes:

  • Android: High priority, custom channel stock_loss_approval, default sound
  • iOS (APNs): Default sound, badge count = 1

Source Files

FilePurpose
backend/Almafrica.Infrastructure/Services/PushNotificationService.csFCM HTTP v1 integration
backend/Almafrica.Application/Interfaces/IPushNotificationService.csAbstraction interface

WF-INTEG-08: Mobile Connectivity Detection

Trigger: Device network state change or periodic recheck Frequency: Continuous monitoring + 60-second offline recheck interval Offline Support: This IS the offline detection system Cross-references: WF-SYNC-01 (sync trigger on reconnect), WF-INTEG-02 (chunked upload pause/resume)

Workflow Diagram

Network Type Detection

PriorityConnectivityResultNetworkTypeSuitable for large uploads
1 (highest)wifiNetworkType.wifiYes
2ethernetNetworkType.wifi (treated as wifi)Yes
3mobileNetworkType.cellularCaution
noneNetworkType.noneNo

DNS Probe Hosts

PriorityHostSource
1API server host (parsed from AppConstants.API_BASE_URL)Dynamic
2google.comStatic fallback
3cloudflare.comStatic fallback

Captive Portal Probe URLs

URLMethodExpected Response
http://connectivitycheck.gstatic.com/generate_204HEAD (fallback: GET)HTTP 204 No Content
http://clients3.google.com/generate_204HEAD (fallback: GET)HTTP 204 No Content

Key Implementation Details

  • CancelableOperation: DNS lookups use async/CancelableOperation to prevent orphaned futures when a new connectivity check starts before the previous one completes.
  • Future.any race: DNS lookups are raced in parallel; the first successful lookup short-circuits the rest.
  • Offline recheck timer: When offline, a 60-second periodic timer rechecks connectivity. The timer is cancelled when connectivity is restored.
  • Guard against reentrancy: A _isPeriodicCheckRunning flag prevents overlapping periodic checks.

Source Files

FilePurpose
mobile/mon_jardin/lib/data/services/connectivity_service.dartFull connectivity detection service
mobile/mon_jardin/lib/core/enums/network_type.dartNetwork type enum

Service Dependency Matrix

ServicePostgreSQLRedisDO SpacesAfrica's TalkingFCMSignalRInternet
Auth (login/register)RequiredOTP onlyRequired
Farmer ManagementRequiredPhoto uploadData broadcastRequired (sync)
Client ManagementRequiredPhoto + docsData broadcastRequired (sync)
Quality AssessmentRequiredAssessment photosRequired (sync)
Warehouse/StockRequiredLoss alertsData broadcastRequired (sync)
Campaigns/SurveysRequiredSurvey attachmentsRequired (sync)
Production CyclesRequiredRequired (sync)
Permission SystemRequiredPermission pushRequired
Mobile OfflineReconnect triggerOptional
Push NotificationsRequired (tokens)RequiredRequired
Chunked UploadRequiredRequired

Docker/Deployment Architecture

Container Topology

Environment Matrix

EnvironmentAPI DomainWeb DomainDatabaseCompose FileBuild
Locallocalhost:8080localhost:3000Local or Coolifydocker-compose.local.ymlSource build
Devapi-dev.almafrica.comdev.almafrica.comCoolify-manageddocker-compose.dev.ymlSource build
Stagingapi-staging.almafrica.comstaging.almafrica.comCoolify-manageddocker-compose.staging.ymlSource build
Productionapi.almafrica.comapp.almafrica.comCoolify-manageddocker-compose.production.ymlPre-built GHCR images

API Container Details

PropertyValue
Base image (build)mcr.microsoft.com/dotnet/sdk:10.0
Base image (runtime)mcr.microsoft.com/dotnet/aspnet:10.0
Exposed port5010
Health checkcurl -f http://localhost:5010/health every 30s
Health check start period120s (allows for migration + seeding)
Run asNon-root ($APP_UID)
Restart policyunless-stopped

Secret Injection

All secrets are injected as environment variables by Coolify. The docker-compose files reference them via ${VARIABLE} syntax:

SecretVariableInjected By
Database URLDATABASE_URLCoolify managed PostgreSQL
Redis URLREDIS_URLCoolify managed Redis
JWT signing keyJWT_SECRET_KEYCoolify environment
DO Spaces access keyDO_SPACES_ACCESS_KEYCoolify environment
DO Spaces secret keyDO_SPACES_SECRET_KEYCoolify environment
DO Spaces bucket nameDO_SPACES_NAMECoolify environment
SMS providerSMS_PROVIDERCoolify environment

Integration Health Monitoring

The backend exposes three health endpoints:

EndpointChecksResponse
GET /healthPostgreSQL connectivity via EF Core DbContextCheckStandard ASP.NET health response
GET /health/versionNone (informational){ status, version, environment, buildDate, commitSha }
GET /health/signalrNone (informational){ status: "Healthy", service: "SignalR" }

Workflow Cross-Reference Index

This table maps each integration workflow to the domain workflows that depend on it:

Integration WorkflowDependent Workflows
WF-INTEG-01 (DO Spaces regular upload)WF-FARM-01, WF-FARM-02, WF-CLI-01, WF-CLI-02, WF-QA-01, WF-QA-02, WF-CAMP-03
WF-INTEG-02 (DO Spaces chunked upload)WF-CLI-02, WF-SYNC-03
WF-INTEG-03 (Africa's Talking OTP)WF-AUTH-01, WF-AUTH-04
WF-INTEG-04 (SignalR real-time)WF-AUTH-05, WF-AUTH-06, WF-SYNC-01, WF-STOCK-01, WF-FARM-01
WF-INTEG-05 (PostgreSQL)All WF-AUTH-, WF-FARM-, WF-CLI-, WF-STOCK-, WF-PROD-, WF-CAMP-, WF-QA-*
WF-INTEG-06 (Redis cache)Currently none (disabled); intended for WF-AUTH-01, WF-SYNC-01
WF-INTEG-07 (FCM push)WF-NOTIF-01, WF-STOCK-05
WF-INTEG-08 (Connectivity detection)WF-SYNC-01, WF-INTEG-02, WF-INTEG-04