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
| Setting | Env Variable | Default | Description |
|---|
DigitalOceanSpaces:AccessKey | DO_SPACES_ACCESS_KEY | (required) | S3 access key |
DigitalOceanSpaces:SecretKey | DO_SPACES_SECRET_KEY | (required) | S3 secret key |
DigitalOceanSpaces:SpaceName | DO_SPACES_NAME | (required) | Bucket name |
DigitalOceanSpaces:Region | DO_SPACES_REGION | nyc3 | S3 region |
DigitalOceanSpaces:Endpoint | DO_SPACES_ENDPOINT | https://nyc3.digitaloceanspaces.com | S3 endpoint URL |
DigitalOceanSpaces:CdnEndpoint | DO_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
| File | Purpose |
|---|
backend/Almafrica.Infrastructure/Services/DigitalOceanSpacesService.cs | S3 upload/delete with image processing |
backend/Almafrica.Infrastructure/Services/MediaUploadService.cs | Validation layer (5MB limit, extension whitelist) |
backend/Almafrica.Application/Interfaces/IObjectStorageService.cs | Abstraction 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
| Parameter | Value | Description |
|---|
| Default chunk size | 512 KB | Mobile-side default per part |
| Min chunk size | 100 KB | Backend floor clamp |
| Max chunk size | 5 MB | Backend ceiling clamp |
| Max file size | 50 MB | Total file size limit |
| Part timeout | 60 seconds | Per-chunk upload timeout |
| Max retries per part | 3 | With exponential backoff |
| Max consecutive failures | 10 | Before marking upload as failed |
| Presigned URL expiry | 60 minutes | Configurable via PresignedUrlExpirationMinutes |
| Allowed folders | clients, client-documents, crop-demands, farmers, uploads | Security whitelist |
Resume Flow
Source Files
| File | Purpose |
|---|
mobile/mon_jardin/lib/data/services/chunked_upload_service.dart | Mobile chunked upload orchestrator |
backend/Almafrica.Infrastructure/Services/MultipartUploadService.cs | Backend S3 multipart management |
backend/Almafrica.Application/Interfaces/IMultipartUploadService.cs | Abstraction 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
| Setting | Env Variable | Default | Description |
|---|
Otp:DevelopmentMode | OTP_DEV_MODE | true (dev) / false (prod) | When true, OTP is logged, not sent |
Otp:SmsProvider | SMS_PROVIDER | AfricasTalking | SMS provider identifier |
Otp:ExpiryMinutes | — | 10 | OTP validity window |
Otp:MaxAttempts | — | 5 | Max 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
| File | Purpose |
|---|
backend/Almafrica.Infrastructure/Services/OtpService.cs | OTP generation, verification, and cleanup |
backend/Almafrica.Application/Interfaces/IOtpService.cs | Abstraction 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
| Setting | Value | Description |
|---|
EnableDetailedErrors | true | Detailed error messages in development |
KeepAliveInterval | 15 seconds | Server ping interval |
ClientTimeoutInterval | 30 seconds | Time before considering client disconnected |
| Authentication | [Authorize(AuthenticationSchemes = "Bearer")] | JWT required for hub connection |
| Hub path | /hubs/permissions | WebSocket endpoint |
| Health check | GET /health/signalr | Returns { status: "Healthy", service: "SignalR" } |
Message Types
| Method | Direction | Payload | Purpose |
|---|
PermissionsUpdated | Server to User | { userId, permissions[], timestamp } | Targeted permission update |
DataChanged | Server to All | { entityType, entityId, action, timestamp } | Broadcast data change notification |
GetConnectionId | Client invoke | Returns string | Client can query its own connection ID |
Mobile Reconnect Strategy
| Attempt | Delay | Max |
|---|
| 1 | 1 second | — |
| 2 | 2 seconds | — |
| 3 | 4 seconds | — |
| 4 | 8 seconds | — |
| 5 | 16 seconds | — |
| 6+ | 30 seconds | Capped |
Source Files
| File | Purpose |
|---|
backend/Almafrica.API/Hubs/PermissionHub.cs | SignalR hub with JWT auth, connection logging |
backend/Almafrica.API/Services/PermissionBroadcastService.cs | Server-side broadcast logic |
backend/Almafrica.Application/Interfaces/IPermissionBroadcastService.cs | Abstraction interface |
mobile/mon_jardin/lib/data/services/permission_sync_service.dart | Mobile SignalR client |
backend/Almafrica.API/Extensions/ServiceCollectionExtensions.cs | SignalR DI registration |
backend/Almafrica.API/Extensions/WebApplicationExtensions.cs | Hub 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:
ConnectionStrings:almafricadb (standard .NET config)
ConnectionStrings__almafricadb (Coolify double-underscore format)
- Automatic conversion of
postgres:// URIs to Npgsql format (for Coolify-managed PostgreSQL)
EF Core Configuration
| Setting | Value | Description |
|---|
| Provider | Npgsql (PostgreSQL) | Primary database |
| Dynamic JSON | Enabled | List<T> serialized as jsonb columns |
| History table | __efmigrations_history | Snake_case naming |
| Interceptor | AuditSaveChangesInterceptor | Immutable audit trail on every save |
| Naming | Snake_case | Custom SnakeCaseHistoryRepository |
Migration Strategy
| Setting | Value | Description |
|---|
| Auto-migrate on startup | Configurable via RunMigrations | Default: true |
| Retry attempts | 10 | For container startup race conditions |
| Retry delay | 3 seconds | Between attempts |
| Production override | RunMigrations: false | Disabled 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
| Environment | Image | Port | Volume | Profile |
|---|
| Local | redis:7-alpine | 6379 | redis_dev_data | local-cache (opt-in) |
| Dev/Staging | Coolify-managed | Injected via REDIS_URL | Managed | Always available |
| Production | Coolify-managed | Injected via REDIS_URL | Managed | Always 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
| Setting | Env Variable | Default | Description |
|---|
Firebase:ProjectId | — | (empty) | GCP project ID |
Firebase:CredentialsPath | — | (empty) | Path to service account JSON |
Firebase:Enabled | — | false | Master 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
| File | Purpose |
|---|
backend/Almafrica.Infrastructure/Services/PushNotificationService.cs | FCM HTTP v1 integration |
backend/Almafrica.Application/Interfaces/IPushNotificationService.cs | Abstraction 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
| Priority | ConnectivityResult | NetworkType | Suitable for large uploads |
|---|
| 1 (highest) | wifi | NetworkType.wifi | Yes |
| 2 | ethernet | NetworkType.wifi (treated as wifi) | Yes |
| 3 | mobile | NetworkType.cellular | Caution |
| — | none | NetworkType.none | No |
DNS Probe Hosts
| Priority | Host | Source |
|---|
| 1 | API server host (parsed from AppConstants.API_BASE_URL) | Dynamic |
| 2 | google.com | Static fallback |
| 3 | cloudflare.com | Static fallback |
Captive Portal Probe URLs
| URL | Method | Expected Response |
|---|
http://connectivitycheck.gstatic.com/generate_204 | HEAD (fallback: GET) | HTTP 204 No Content |
http://clients3.google.com/generate_204 | HEAD (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
| File | Purpose |
|---|
mobile/mon_jardin/lib/data/services/connectivity_service.dart | Full connectivity detection service |
mobile/mon_jardin/lib/core/enums/network_type.dart | Network type enum |
Service Dependency Matrix
| Service | PostgreSQL | Redis | DO Spaces | Africa's Talking | FCM | SignalR | Internet |
|---|
| Auth (login/register) | Required | — | — | OTP only | — | — | Required |
| Farmer Management | Required | — | Photo upload | — | — | Data broadcast | Required (sync) |
| Client Management | Required | — | Photo + docs | — | — | Data broadcast | Required (sync) |
| Quality Assessment | Required | — | Assessment photos | — | — | — | Required (sync) |
| Warehouse/Stock | Required | — | — | — | Loss alerts | Data broadcast | Required (sync) |
| Campaigns/Surveys | Required | — | Survey attachments | — | — | — | Required (sync) |
| Production Cycles | Required | — | — | — | — | — | Required (sync) |
| Permission System | Required | — | — | — | — | Permission push | Required |
| Mobile Offline | — | — | — | — | — | Reconnect trigger | Optional |
| Push Notifications | Required (tokens) | — | — | — | Required | — | Required |
| Chunked Upload | — | — | Required | — | — | — | Required |
Docker/Deployment Architecture
Container Topology
Environment Matrix
| Environment | API Domain | Web Domain | Database | Compose File | Build |
|---|
| Local | localhost:8080 | localhost:3000 | Local or Coolify | docker-compose.local.yml | Source build |
| Dev | api-dev.almafrica.com | dev.almafrica.com | Coolify-managed | docker-compose.dev.yml | Source build |
| Staging | api-staging.almafrica.com | staging.almafrica.com | Coolify-managed | docker-compose.staging.yml | Source build |
| Production | api.almafrica.com | app.almafrica.com | Coolify-managed | docker-compose.production.yml | Pre-built GHCR images |
API Container Details
| Property | Value |
|---|
| Base image (build) | mcr.microsoft.com/dotnet/sdk:10.0 |
| Base image (runtime) | mcr.microsoft.com/dotnet/aspnet:10.0 |
| Exposed port | 5010 |
| Health check | curl -f http://localhost:5010/health every 30s |
| Health check start period | 120s (allows for migration + seeding) |
| Run as | Non-root ($APP_UID) |
| Restart policy | unless-stopped |
Secret Injection
All secrets are injected as environment variables by Coolify. The docker-compose files reference them via ${VARIABLE} syntax:
| Secret | Variable | Injected By |
|---|
| Database URL | DATABASE_URL | Coolify managed PostgreSQL |
| Redis URL | REDIS_URL | Coolify managed Redis |
| JWT signing key | JWT_SECRET_KEY | Coolify environment |
| DO Spaces access key | DO_SPACES_ACCESS_KEY | Coolify environment |
| DO Spaces secret key | DO_SPACES_SECRET_KEY | Coolify environment |
| DO Spaces bucket name | DO_SPACES_NAME | Coolify environment |
| SMS provider | SMS_PROVIDER | Coolify environment |
Integration Health Monitoring
The backend exposes three health endpoints:
| Endpoint | Checks | Response |
|---|
GET /health | PostgreSQL connectivity via EF Core DbContextCheck | Standard ASP.NET health response |
GET /health/version | None (informational) | { status, version, environment, buildDate, commitSha } |
GET /health/signalr | None (informational) | { status: "Healthy", service: "SignalR" } |
Workflow Cross-Reference Index
This table maps each integration workflow to the domain workflows that depend on it:
| Integration Workflow | Dependent 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 |