2026-05-31·5 min read·sota.io Team

EU Data Act Cloud Switching 2026: Complete Technical Implementation Guide for SaaS Providers

Post #1416 in the sota.io EU Cloud Compliance Series

EU Data Act Cloud Switching Technical Implementation Guide 2026

The EU Data Act (Regulation (EU) 2023/2854) has been fully applicable since September 12, 2025. Unlike compliance deadlines that approach on the horizon, cloud switching obligations are already in force — and enforcement has begun. If your SaaS platform holds customer data, this guide explains exactly what you must build: the switching request API, the data export pipeline, the 30-day state machine, and how fee compliance works through 2027.

This is the first post in our EU-DATA-ACT-CLOUD-SWITCHING-2026 series. We cover the technical implementation that legal summaries skip.


What the Cloud Switching Obligation Means for Developers

The Data Act introduces a right to switch cloud providers — not just for end-users, but for all business customers of data processing services. The obligation falls on cloud service providers (CSPs), which includes SaaS platforms, PaaS, and IaaS services that:

If your SaaS platform stores customer data that is not easily exportable via self-service, you are within scope. The regulation does not require a minimum scale or transaction volume — it applies to all CSPs operating in the EU market.

Core obligations (already in force since September 2025):

  1. Portability within 30 days — When a customer requests to switch providers, you must export all their data in a machine-readable, interoperable format within 30 calendar days of the formal switch request.
  2. Machine-readable export documentation — The export mechanism must be documented in a machine-readable format (OpenAPI, JSON Schema, or equivalent) so the receiving provider can import without manual intervention.
  3. 30-day post-switch retention — After export, you must retain a complete copy of the exported data for 30 days in case the customer needs to retrieve anything.
  4. No functional degradation — You cannot reduce service quality, remove features, or restrict API access during the switching process in response to the switch request.
  5. Fee transparency — Switching fees (if any) must be disclosed upfront and cannot exceed costs directly attributable to the switching process.

The fee prohibition timeline:


Designing the Switching Request API

The switching process begins with a formal switch request. Unlike a GDPR data subject access request (which can be informal), the Data Act contemplates a more structured process. Your API must support:

Switch Request Endpoint

// POST /api/v1/data-portability/switch-request
// Creates a formal Data Act switching request

interface SwitchRequest {
  requestType: "full_export" | "partial_export";
  requestedBy: string;           // customer account ID
  targetProvider?: string;       // optional: name of receiving CSP
  dataScopes: DataScope[];       // what data to export
  preferredFormat: ExportFormat;
  scheduledStartDate?: string;   // ISO 8601, defaults to now
  contactEmail: string;          // for notifications
}

type DataScope =
  | "all"                        // full account export
  | "user_data"                  // profile, settings, preferences
  | "application_data"           // workspace content, files
  | "metadata"                   // labels, tags, audit logs
  | "configurations"             // app config, integrations, webhooks
  | "billing_history";           // invoices and payment records

type ExportFormat =
  | "json"
  | "csv"
  | "jsonld"
  | "interoperable";             // provider-defined open format

interface SwitchRequestResponse {
  requestId: string;             // UUID for tracking
  status: "accepted";
  deadlineDate: string;          // ISO 8601: request date + 30 days
  estimatedReadyDate: string;    // when export will be available
  statusUrl: string;             // polling endpoint
  exportFee?: SwitchFee;         // must be disclosed upfront
}

interface SwitchFee {
  amountCents: number;           // 0 after September 2027
  currency: "EUR";
  feeType: "cost_recovery";      // never "lock_in" or "egress_profit"
  breakdown: FeeLineItem[];
}

The 30-Day State Machine

The switching process has defined states that must be tracked and communicated:

type SwitchState =
  | "received"         // request accepted, awaiting processing
  | "preparing"        // data export generation in progress
  | "ready_for_pickup" // export package available for download
  | "transferred"      // customer has downloaded/transferred data
  | "retention_period" // 30-day post-transfer retention window
  | "completed"        // retention window expired, process closed
  | "cancelled";       // customer cancelled before transfer

interface SwitchStateTransition {
  fromState: SwitchState;
  toState: SwitchState;
  triggerCondition: string;
  maxDuration: string;           // ISO 8601 duration
}

const STATE_MACHINE: SwitchStateTransition[] = [
  {
    fromState: "received",
    toState: "preparing",
    triggerCondition: "automatic within 1 business day",
    maxDuration: "P1D"
  },
  {
    fromState: "preparing",
    toState: "ready_for_pickup",
    triggerCondition: "export package generated",
    maxDuration: "P29D"          // must stay within 30-day total
  },
  {
    fromState: "ready_for_pickup",
    toState: "transferred",
    triggerCondition: "customer downloads export",
    maxDuration: "P30D"          // pickup window for customer
  },
  {
    fromState: "transferred",
    toState: "retention_period",
    triggerCondition: "automatic after download confirmation",
    maxDuration: "P0D"
  },
  {
    fromState: "retention_period",
    toState: "completed",
    triggerCondition: "30 days after transfer",
    maxDuration: "P30D"
  }
];

class SwitchRequestManager {
  async checkDeadlineCompliance(requestId: string): Promise<boolean> {
    const request = await this.getRequest(requestId);
    const deadlineMs = new Date(request.deadlineDate).getTime();
    const nowMs = Date.now();

    if (
      request.state === "received" ||
      request.state === "preparing"
    ) {
      const remainingMs = deadlineMs - nowMs;
      if (remainingMs < 0) {
        // BREACH: 30-day deadline exceeded — log for compliance
        await this.logComplianceBreach(requestId, "30_day_deadline_exceeded");
        return false;
      }
      // Alert if less than 3 days remain
      if (remainingMs < 3 * 24 * 60 * 60 * 1000) {
        await this.sendDeadlineAlert(requestId, remainingMs);
      }
    }
    return true;
  }

  async getRequest(requestId: string): Promise<SwitchRequestRecord> {
    // implementation
    throw new Error("Not implemented");
  }

  async logComplianceBreach(requestId: string, reason: string): Promise<void> {
    // log to compliance audit trail
  }

  async sendDeadlineAlert(requestId: string, remainingMs: number): Promise<void> {
    // notify ops team
  }
}

Data Export Format Specification

The Data Act requires exports in a format that allows the receiving provider to import without manual intervention. This rules out:

Compliant formats:

interface DataActExportPackage {
  metadata: ExportMetadata;
  manifest: ExportManifest;
  dataObjects: DataObject[];
}

interface ExportMetadata {
  exportFormatVersion: "1.0";
  regulation: "EU-2023-2854";             // Data Act reference
  exportedAt: string;                     // ISO 8601
  sourceProvider: string;
  accountId: string;
  retentionDeadline: string;              // 30 days after export
  totalObjectCount: number;
  totalSizeBytes: number;
}

interface ExportManifest {
  schemaUrl: string;                      // machine-readable schema (OpenAPI/JSON Schema)
  dataTypes: DataTypeManifestEntry[];
}

interface DataTypeManifestEntry {
  dataType: string;                       // e.g. "workspace", "user_profile"
  objectCount: number;
  schemaVersion: string;
  importDocumentationUrl: string;         // where receiving provider finds import guide
}

interface DataObject {
  type: string;
  id: string;
  createdAt: string;
  updatedAt: string;
  data: Record<string, unknown>;         // type-specific payload
  relationships?: DataObjectRelationship[];
}

Interoperability in Practice: The SWIPO Code of Conduct

The SWIPO (Switching and Porting Initiative) IaaS and SaaS codes of conduct define the practical interoperability standards. Adherence to SWIPO is a strong indicator of Data Act compliance for the export format requirement.

For SaaS providers, SWIPO's SaaS-specific code focuses on:

  1. Scope documentation — published list of what data is and is not exportable
  2. Format documentation — publicly available schema for exported data types
  3. Import guidance — instructions for receiving providers on how to ingest the export
  4. Test exports — ability to generate a sample export without triggering the full switching process
// Endpoint for format documentation (publicly accessible, no auth)
// GET /api/v1/data-portability/export-schema
interface ExportSchemaDocumentation {
  version: "1.0";
  lastUpdated: string;
  exportableScopes: {
    scope: DataScope;
    format: string;
    schemaUrl: string;
    importDocumentationUrl: string;
  }[];
  nonExportableData: {
    dataType: string;
    reason: string;     // "third_party_license", "security_sensitive", etc.
  }[];
  testExportUrl: string;
}

Building the Data Export Pipeline

The actual data export must handle large accounts efficiently while staying within the 30-day deadline. For most SaaS platforms, the bottleneck is generating consistent, relational exports across distributed data stores.

// Export pipeline implementation
class DataExportPipeline {
  constructor(
    private readonly db: DatabaseClient,
    private readonly storage: ObjectStorage,
    private readonly eventBus: EventBus
  ) {}

  async startExport(request: SwitchRequestRecord): Promise<void> {
    const exportId = `export_${request.requestId}`;

    await this.eventBus.publish("switch.export.started", {
      requestId: request.requestId,
      exportId,
      startedAt: new Date().toISOString()
    });

    // Export in stages to avoid memory pressure on large accounts
    const stages: ExportStage[] = [
      { name: "user_profiles", extractor: () => this.extractUserProfiles(request.accountId) },
      { name: "workspace_data", extractor: () => this.extractWorkspaceData(request.accountId) },
      { name: "configurations", extractor: () => this.extractConfigurations(request.accountId) },
      { name: "audit_logs", extractor: () => this.extractAuditLogs(request.accountId) },
    ];

    const manifest: ExportManifest = {
      schemaUrl: "https://your-saas.io/api/v1/data-portability/export-schema",
      dataTypes: []
    };

    for (const stage of stages) {
      const objects = await stage.extractor();
      const partPath = `${exportId}/${stage.name}.json`;

      await this.storage.put(partPath, JSON.stringify(objects));

      manifest.dataTypes.push({
        dataType: stage.name,
        objectCount: objects.length,
        schemaVersion: "1.0",
        importDocumentationUrl: `https://your-saas.io/docs/import/${stage.name}`
      });
    }

    // Build the final package manifest
    const packageMetadata: ExportMetadata = {
      exportFormatVersion: "1.0",
      regulation: "EU-2023-2854",
      exportedAt: new Date().toISOString(),
      sourceProvider: "your-saas.io",
      accountId: request.accountId,
      retentionDeadline: new Date(
        Date.now() + 30 * 24 * 60 * 60 * 1000  // now + 30 days
      ).toISOString(),
      totalObjectCount: manifest.dataTypes.reduce((s, t) => s + t.objectCount, 0),
      totalSizeBytes: await this.storage.getDirectorySize(exportId)
    };

    await this.storage.put(`${exportId}/manifest.json`, JSON.stringify({
      metadata: packageMetadata,
      manifest
    }));

    await this.updateSwitchRequestState(request.requestId, "ready_for_pickup", {
      downloadUrl: await this.storage.getPresignedUrl(`${exportId}/`, { expiresIn: 30 * 24 * 3600 }),
      retentionDeadline: packageMetadata.retentionDeadline
    });
  }

  private async extractUserProfiles(accountId: string): Promise<DataObject[]> {
    // implementation
    return [];
  }

  private async extractWorkspaceData(accountId: string): Promise<DataObject[]> {
    // implementation
    return [];
  }

  private async extractConfigurations(accountId: string): Promise<DataObject[]> {
    // implementation
    return [];
  }

  private async extractAuditLogs(accountId: string): Promise<DataObject[]> {
    // implementation
    return [];
  }

  private async updateSwitchRequestState(
    requestId: string,
    state: SwitchState,
    metadata: Record<string, unknown>
  ): Promise<void> {
    // implementation
  }
}

type SwitchState = "received" | "preparing" | "ready_for_pickup" | "transferred" | "retention_period" | "completed" | "cancelled";
type DataScope = "all" | "user_data" | "application_data" | "metadata" | "configurations" | "billing_history";

interface SwitchRequestRecord {
  requestId: string;
  accountId: string;
  state: SwitchState;
  createdAt: string;
  deadlineDate: string;
}

interface ExportStage {
  name: string;
  extractor: () => Promise<DataObject[]>;
}

Fee Compliance Architecture

Your billing system must enforce the phased fee prohibition. Building this as a policy engine (rather than hardcoded pricing) makes it easier to update as each phase kicks in.

// Fee policy engine for Data Act compliance
class SwitchingFeePolicy {
  private readonly PHASE_1_DATE = new Date("2027-01-12"); // fee capped at cost
  private readonly PHASE_2_DATE = new Date("2027-09-12"); // fee prohibited

  calculateAllowedFee(
    requestDate: Date,
    actualCostCents: number,
    requestedFeesCents: number
  ): SwitchFeeDecision {
    if (requestDate >= this.PHASE_2_DATE) {
      return {
        allowedFeeCents: 0,
        policyPhase: "phase_2_prohibited",
        explanation: "All switching fees prohibited from September 12, 2027"
      };
    }

    if (requestDate >= this.PHASE_1_DATE) {
      // Fee capped at actual cost — no profit margin allowed
      const cappedFee = Math.min(requestedFeesCents, actualCostCents);
      return {
        allowedFeeCents: cappedFee,
        policyPhase: "phase_1_cost_only",
        explanation: "Fee capped at actual cost recovery from January 12, 2027"
      };
    }

    // Current regime (until January 2027): fees allowed but must be disclosed
    return {
      allowedFeeCents: requestedFeesCents,
      policyPhase: "current_disclosed",
      explanation: "Fee allowed if disclosed at contract signing"
    };
  }

  requiresFeeDisclosure(requestDate: Date): boolean {
    return requestDate < this.PHASE_1_DATE;
  }
}

interface SwitchFeeDecision {
  allowedFeeCents: number;
  policyPhase: "current_disclosed" | "phase_1_cost_only" | "phase_2_prohibited";
  explanation: string;
}

Data Act vs. GDPR Portability: Key Technical Differences

Many teams conflate GDPR Article 20 data portability with the Data Act switching obligation. They are separate rights with different technical requirements:

DimensionGDPR Art.20 PortabilityData Act Switching
Who can invokeNatural persons (data subjects)Business customers (organisations)
TriggerAny time, informal requestFormal switch request
ScopePersonal data onlyAll customer data (including non-personal)
Timeline1 month (GDPR Art.12)30 days
Format"Commonly used, machine-readable"Interoperable, must allow import without manual steps
Post-transfer retentionNo explicit requirement30 days
FeeFreeFee allowed until Jan 2027, then prohibited
EnforcementDPAsNational competent authorities under Data Act

Implementation implication: Your GDPR portability endpoint and your Data Act switching API must be distinct, even if they share underlying export infrastructure. They have different authentication paths (individual vs. account admin), different scopes, and different response format requirements.

// Two separate endpoints — shared extraction, different wrappers
// GDPR: GET /api/v1/gdpr/portability-export (personal data only, individual auth)
// Data Act: POST /api/v1/data-portability/switch-request (full account, admin auth)

Monitoring and Compliance Reporting

The Data Act requires that switching obligations can be audited. Build compliance reporting into your switch request lifecycle:

// Compliance metrics you must be able to produce on request
interface DataActComplianceReport {
  reportingPeriod: { start: string; end: string };
  switchRequestsReceived: number;
  switchRequestsCompletedOnTime: number;     // within 30 days
  switchRequestsBreachedDeadline: number;    // non-compliant
  averageCompletionDays: number;
  feesCharged: { count: number; totalCents: number };
  openRequests: number;
  retentionPeriodActive: number;
}

// Alert thresholds for operational monitoring
const COMPLIANCE_ALERTS = {
  daysUntilDeadlineWarning: 5,    // alert ops team when 5 days remain
  daysUntilDeadlineCritical: 1,   // page on-call when 1 day remains
  maxOpenRequestsBeforeAlert: 50, // flag if queue is growing
};

Production Checklist: Data Act Cloud Switching Compliance

API and endpoints:

Data export:

Timeline enforcement:

Fee compliance:

Separation from GDPR portability:

Operational:


What Comes Next in This Series

This is Post #1416 — the first post in our EU-DATA-ACT-CLOUD-SWITCHING-2026 series:

  1. #1416 (this post): Cloud switching technical implementation guide
  2. #1417: Data portability API design — REST patterns, OpenAPI specs, import/export schema
  3. #1418: B2B data sharing obligations for IoT and connected product data
  4. #1419: EU Data Act vs. GDPR dual-compliance architecture
  5. #1420: EU Data Act compliance finale — complete SaaS developer toolkit 2026

Key Takeaways

The Data Act's cloud switching framework is engineering work — legal summaries help you understand the obligation, but this implementation guide gives you the code.

EU-Native Hosting

Ready to move to EU-sovereign infrastructure?

sota.io is a German-hosted PaaS — no CLOUD Act exposure, no US jurisdiction, full GDPR compliance by design. Deploy your first app in minutes.