Skip to main content
The skeleton template uses a Factory pattern with standardized interfaces to provide cloud portability. Your application code remains unchanged regardless of the cloud provider.

How Multi-Cloud Portability Works

The platform uses a service factory that selects the correct cloud provider implementation based on the CLOUD_PROVIDER environment variable:
Application (routes, controllers)
    |
    v
ServiceFactory.getStorageService()   <-- reads CLOUD_PROVIDER from .env
    |
    +-- CLOUD_PROVIDER=aws   --> AwsStorageService
    +-- CLOUD_PROVIDER=oci   --> OciStorageService
    +-- CLOUD_PROVIDER=gcp   --> GcpStorageService
    +-- CLOUD_PROVIDER=azure --> AzureStorageService
To migrate from AWS to OCI: change CLOUD_PROVIDER=oci in .env and implement the 4 OCI services. The rest of your application requires no changes.

Service Provider Mapping

InterfaceAWSOCIGCPAzure
StorageServiceS3Object StorageCloud StorageBlob Storage
DatabaseServiceRDS PostgreSQLDB ServiceCloud SQLDB for PostgreSQL
MonitoringServiceCloudWatchMonitoringCloud OperationsAzure Monitor
AuthServiceIAM / CognitoIdentity Cloud ServiceFirebase Auth / IAMAzure Active Directory
Container RegistryECROCIRArtifact RegistryACR
KubernetesEKSOKEGKEAKS
Load BalancerALB / ELBLoad BalancerCloud Load BalancingAzure Load Balancer
SecretsSecrets ManagerVault ServiceSecret ManagerKey Vault

Backend Service Structure

All cloud provider implementations follow this structure in app/backend/src/services/:
src/services/
  factory.js                          <-- selects provider based on CLOUD_PROVIDER
  providers/
    aws/
      aws-storage.service.js
      aws-database.service.js
      aws-monitoring.service.js
      aws-auth.service.js
    oci/
      oci-storage.service.js
      oci-database.service.js
      oci-monitoring.service.js
      oci-auth.service.js
    gcp/
      gcp-storage.service.js
      gcp-database.service.js
      gcp-monitoring.service.js
      gcp-auth.service.js
    azure/
      azure-storage.service.js
      azure-database.service.js
      azure-monitoring.service.js
      azure-auth.service.js

Using Cloud Services in Your Application

StorageService (S3 / Object Storage)

Upload, download, and manage files in cloud object storage:
const { getStorageService } = require('./services/factory');

const storage = getStorageService();

// Upload a file
await storage.uploadFile(
  'my-bucket',
  'documents/report.pdf',
  fileBuffer,
  'application/pdf'
);

// Download a file
const fileBuffer = await storage.downloadFile('my-bucket', 'documents/report.pdf');

// Generate a temporary download URL (expires in 1 hour)
const url = await storage.getSignedUrl('my-bucket', 'documents/report.pdf', 3600);

// List files with a prefix
const files = await storage.listFiles('my-bucket', 'documents/');
// Returns: [{ key, size, lastModified }]

// Delete a file
await storage.deleteFile('my-bucket', 'documents/old-file.pdf');
class StorageService {
  // Upload a file to bucket/container
  async uploadFile(bucketName, key, body, contentType) {}

  // Download a file
  async downloadFile(bucketName, key) {}  // returns Buffer

  // Delete a file
  async deleteFile(bucketName, key) {}

  // List files with optional prefix
  async listFiles(bucketName, prefix = '') {}  // returns [{ key, size, lastModified }]

  // Generate temporary access URL (pre-signed URL)
  async getSignedUrl(bucketName, key, expiresInSeconds = 3600) {}  // returns string URL
}

DatabaseService (RDS / Cloud SQL)

Execute SQL queries with transaction support:
const { getDatabaseService } = require('./services/factory');

const db = getDatabaseService();

// Execute a query
const result = await db.query(
  'SELECT * FROM users WHERE status = $1',
  ['active']
);
console.log(result.rows);

// Use transactions
const client = await db.beginTransaction();
try {
  await db.query('INSERT INTO users (name, email) VALUES ($1, $2)', ['John', '[email protected]'], client);
  await db.query('INSERT INTO audit_log (action) VALUES ($1)', ['user_created'], client);
  await db.commitTransaction(client);
} catch (error) {
  await db.rollbackTransaction(client);
  throw error;
}

// Health check (for Kubernetes probes)
const health = await db.ping();
// Returns: { status: 'healthy' | 'unhealthy', latencyMs: 45 }
class DatabaseService {
  // Execute a SQL query
  async query(sql, params = []) {}  // returns { rows: [], rowCount: number }

  // Start a transaction
  async beginTransaction() {}

  // Commit a transaction
  async commitTransaction(client) {}

  // Rollback a transaction
  async rollbackTransaction(client) {}

  // Verify connection (for health checks)
  async ping() {}  // returns { status: 'healthy' | 'unhealthy', latencyMs: number }
}

MonitoringService (CloudWatch / Cloud Monitoring)

Record metrics, logs, and create alarms:
const { getMonitoringService } = require('./services/factory');

const monitoring = getMonitoringService();

// Record a metric
await monitoring.putMetric(
  'MyApp',
  'RequestCount',
  1,
  'Count'
);

// Log an event
await monitoring.logEvent(
  '/aws/govtech/backend',
  'User login successful',
  'INFO'
);

// Create an alarm
await monitoring.createAlarm(
  'HighErrorRate',
  'ErrorCount',
  10,
  'GreaterThanThreshold'
);

// Get recent metrics for dashboard
const metrics = await monitoring.getMetrics('MyApp', 'RequestCount', 60);
// Returns: [{ timestamp, value }]
class MonitoringService {
  // Record a numeric metric
  async putMetric(namespace, metricName, value, unit = 'Count') {}

  // Record an event in logs
  async logEvent(logGroup, message, level = 'INFO') {}

  // Create or update an alarm
  async createAlarm(alarmName, metricName, threshold, comparison) {}

  // Get recent metrics (for health dashboard)
  async getMetrics(namespace, metricName, periodMinutes = 60) {}
  // returns [{ timestamp, value }]
}

AuthService (IAM / Cognito / Azure AD)

Handle authentication and authorization:
const { getAuthService } = require('./services/factory');

const auth = getAuthService();

// Verify a token
const tokenInfo = await auth.verifyToken(req.headers.authorization);
if (tokenInfo.valid) {
  console.log('User ID:', tokenInfo.userId);
  console.log('Roles:', tokenInfo.roles);
}

// Check user permissions
const permissions = await auth.getUserPermissions('user-123');
if (permissions.includes('write:reports')) {
  // User can write reports
}

// Generate a session token
const token = await auth.generateToken('user-123', ['admin', 'editor']);

// Revoke a token
await auth.revokeToken(oldToken);
class AuthService {
  // Verify if a token is valid
  async verifyToken(token) {}  // returns { userId, roles, valid: boolean }

  // Get user permissions
  async getUserPermissions(userId) {}  // returns ['read:documents', 'write:reports', ...]

  // Generate a session token
  async generateToken(userId, roles = []) {}  // returns string token

  // Revoke a token
  async revokeToken(token) {}
}

Implementing a Cloud Provider

To add support for a new cloud provider (e.g., OCI), implement the four service interfaces.
1
Create Service Files
2
Create the four service implementation files in app/backend/src/services/providers/oci/:
3
  • oci-storage.service.js
  • oci-database.service.js
  • oci-monitoring.service.js
  • oci-auth.service.js
  • 4
    Install Provider SDK
    5
    cd app/backend
    npm install oci-sdk
    
    6
    Implement the Service
    7
    Example: OCI Storage Service
    8
    // src/services/providers/oci/oci-storage.service.js
    const oci = require('oci-sdk');
    
    class OciStorageService {
      constructor() {
        this.provider = new oci.common.ConfigFileAuthenticationDetailsProvider();
        this.client = new oci.objectstorage.ObjectStorageClient({
          authenticationDetailsProvider: this.provider,
        });
        this.namespace = process.env.OCI_NAMESPACE;
      }
    
      async uploadFile(bucketName, key, body, contentType) {
        const request = {
          namespaceName: this.namespace,
          bucketName,
          objectName: key,
          putObjectBody: body,
          contentType,
        };
        return await this.client.putObject(request);
      }
    
      async downloadFile(bucketName, key) {
        const request = {
          namespaceName: this.namespace,
          bucketName,
          objectName: key,
        };
        const response = await this.client.getObject(request);
        return response.value;
      }
    
      async deleteFile(bucketName, key) {
        const request = {
          namespaceName: this.namespace,
          bucketName,
          objectName: key,
        };
        return await this.client.deleteObject(request);
      }
    
      async listFiles(bucketName, prefix = '') {
        const request = {
          namespaceName: this.namespace,
          bucketName,
          prefix,
        };
        const response = await this.client.listObjects(request);
        return response.listObjects.objects.map(obj => ({
          key: obj.name,
          size: obj.size,
          lastModified: obj.timeModified,
        }));
      }
    
      async getSignedUrl(bucketName, key, expiresInSeconds = 3600) {
        const request = {
          namespaceName: this.namespace,
          bucketName,
          createPreauthenticatedRequestDetails: {
            name: `temp-access-${key}`,
            objectName: key,
            accessType: 'ObjectRead',
            timeExpires: new Date(Date.now() + expiresInSeconds * 1000).toISOString(),
          },
        };
        const response = await this.client.createPreauthenticatedRequest(request);
        return `https://objectstorage.${process.env.OCI_REGION}.oraclecloud.com${response.preauthenticatedRequest.accessUri}`;
      }
    }
    
    module.exports = { OciStorageService };
    
    9
    Register in Factory
    10
    Update src/services/factory.js to include the new provider:
    11
    const PROVIDER = process.env.CLOUD_PROVIDER || 'aws';
    
    // Import OCI services
    const { OciStorageService } = require('./providers/oci/oci-storage.service');
    const { OciDatabaseService } = require('./providers/oci/oci-database.service');
    const { OciMonitoringService } = require('./providers/oci/oci-monitoring.service');
    const { OciAuthService } = require('./providers/oci/oci-auth.service');
    
    const PROVIDERS = {
      aws: {
        storage:    () => new (require('./providers/aws/aws-storage.service').AwsStorageService)(),
        database:   () => new (require('./providers/aws/aws-database.service').AwsDatabaseService)(),
        monitoring: () => new (require('./providers/aws/aws-monitoring.service').AwsMonitoringService)(),
        auth:       () => new (require('./providers/aws/aws-auth.service').AwsAuthService)(),
      },
      oci: {
        storage:    () => new OciStorageService(),
        database:   () => new OciDatabaseService(),
        monitoring: () => new OciMonitoringService(),
        auth:       () => new OciAuthService(),
      },
    };
    
    function getService(type) {
      const provider = PROVIDERS[PROVIDER];
      if (!provider) {
        throw new Error(`Provider '${PROVIDER}' not implemented. Options: ${Object.keys(PROVIDERS).join(', ')}`);
      }
      return provider[type]();
    }
    
    module.exports = {
      getStorageService:    () => getService('storage'),
      getDatabaseService:   () => getService('database'),
      getMonitoringService: () => getService('monitoring'),
      getAuthService:       () => getService('auth'),
    };
    
    12
    Configure Environment Variables
    13
    Add provider-specific variables to .env:
    14
    AWS
    CLOUD_PROVIDER=aws
    AWS_REGION=us-east-1
    AWS_ACCOUNT_ID=835960996869
    # Credentials injected via IRSA in Kubernetes
    
    OCI
    CLOUD_PROVIDER=oci
    OCI_REGION=us-ashburn-1
    OCI_TENANCY_ID=ocid1.tenancy.oc1..xxx
    OCI_USER_ID=ocid1.user.oc1..xxx
    OCI_FINGERPRINT=xx:xx:xx:xx
    OCI_PRIVATE_KEY_PATH=/secrets/oci-private-key.pem
    OCI_NAMESPACE=govtech-namespace
    
    GCP
    CLOUD_PROVIDER=gcp
    GCP_PROJECT_ID=govtech-prod
    GCP_REGION=us-central1
    GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-sa-key.json
    
    Azure
    CLOUD_PROVIDER=azure
    AZURE_TENANT_ID=xxx
    AZURE_CLIENT_ID=xxx
    AZURE_SUBSCRIPTION_ID=xxx
    AZURE_STORAGE_ACCOUNT=govtechstorage
    
    15
    Test the Implementation
    16
    # Set the cloud provider
    export CLOUD_PROVIDER=oci
    
    # Run tests
    npm test
    
    # Test locally with Docker Compose
    docker-compose up backend
    

    Cloud Provider Migration

    To migrate from one cloud provider to another:
    1
    Prepare Infrastructure in Target Cloud
    2
    cd terraform/environments/prod
    # Create new OCI-specific configuration
    terraform init
    terraform plan -var="cloud_provider=oci"
    terraform apply
    
    3
    Migrate Data
    4
    # S3 (AWS) to OCI Object Storage
    rclone sync s3:govtech-documents oci:govtech-documents
    
    # RDS PostgreSQL to OCI Database Service
    pg_dump -h <rds-endpoint> govtech_prod | \
      psql -h <oci-db-endpoint> govtech_prod
    
    5
    Deploy with New Provider
    6
    # Update environment variable
    kubectl set env deployment/govtech-backend CLOUD_PROVIDER=oci
    
    # Monitor rollout
    kubectl rollout status deployment/govtech-backend
    

    What Changes vs. What Stays the Same

    Unchanged During Migration

    • Application code (Node.js / React)
    • PostgreSQL database schema
    • Kubernetes manifests (except storage classes)
    • CI/CD pipelines (only registry destination changes)
    • Security and compliance policies

    Changed During Migration

    • Environment variables (endpoint, region, credentials)
    • IAM policies (each cloud has its own model)
    • Ingress configuration (ALB on AWS, Cloud Load Balancing on GCP, etc.)
    • Kubernetes storage classes (gp3 on AWS, pd-ssd on GCP, etc.)

    Next Steps

    Build docs developers (and LLMs) love