EU Data Act Cloud Switching 2026: Complete Technical Implementation Guide for SaaS Providers
Post #1416 in the sota.io EU Cloud Compliance Series
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:
- Process, store, or otherwise handle customer data
- Charge for data access, compute, or storage
- Act as the primary custodian of customer-generated data
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):
- 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.
- 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.
- 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.
- No functional degradation — You cannot reduce service quality, remove features, or restrict API access during the switching process in response to the switch request.
- Fee transparency — Switching fees (if any) must be disclosed upfront and cannot exceed costs directly attributable to the switching process.
The fee prohibition timeline:
- January 12, 2027: Switching fees capped at actual cost — no profit margin, no lock-in pricing
- September 12, 2027: All switching fees prohibited — export and migration services must be free
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:
- PDF exports of structured data — not machine-readable
- Custom binary formats without a published specification
- Exports that require the customer to reformat data before it can be ingested elsewhere
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:
- Scope documentation — published list of what data is and is not exportable
- Format documentation — publicly available schema for exported data types
- Import guidance — instructions for receiving providers on how to ingest the export
- 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:
| Dimension | GDPR Art.20 Portability | Data Act Switching |
|---|---|---|
| Who can invoke | Natural persons (data subjects) | Business customers (organisations) |
| Trigger | Any time, informal request | Formal switch request |
| Scope | Personal data only | All customer data (including non-personal) |
| Timeline | 1 month (GDPR Art.12) | 30 days |
| Format | "Commonly used, machine-readable" | Interoperable, must allow import without manual steps |
| Post-transfer retention | No explicit requirement | 30 days |
| Fee | Free | Fee allowed until Jan 2027, then prohibited |
| Enforcement | DPAs | National 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:
-
POST /api/v1/data-portability/switch-requestendpoint exists and accepts formal requests -
GET /api/v1/data-portability/switch-request/:id/statusreturns current state and deadlineDate -
GET /api/v1/data-portability/export-schemapublicly accessible (no auth required) — documents all exportable data types and formats - Switch request triggers an acknowledgement email within 24 hours
Data export:
- Export covers 100% of customer-owned data (no silent omissions)
- Non-exportable data types are documented with reasons
- Export format is documented with a publicly accessible schema (JSON Schema or OpenAPI)
- Export package includes an
importDocumentationUrlfor each data type - Test export endpoint available for receiving providers to validate import
Timeline enforcement:
- Automated deadline tracking — every request has a computed
deadlineDate - Alert sent to ops team at
deadline - 5 days - On-call page triggered at
deadline - 24 hours - Compliance breach logged and alerted if deadline is exceeded
- 30-day post-transfer retention enforced by scheduled job
Fee compliance:
- Fee policy engine implements both Phase 1 (Jan 2027) and Phase 2 (Sep 2027) transitions
- All fees disclosed to customers at contract signing or account creation
- No fees charged for switching requests received after September 12, 2027
- Fee breakdown available to customer on request
Separation from GDPR portability:
- Data Act switch request API and GDPR portability API are distinct endpoints
- Data Act export includes non-personal data; GDPR export is personal data only
- Response format meets the "importable without manual intervention" standard
Operational:
- Monthly compliance report can be generated on demand
- Switching process does not reduce service quality or restrict API access
- Post-switch account access works normally during the 30-day retention window
What Comes Next in This Series
This is Post #1416 — the first post in our EU-DATA-ACT-CLOUD-SWITCHING-2026 series:
- #1416 (this post): Cloud switching technical implementation guide
- #1417: Data portability API design — REST patterns, OpenAPI specs, import/export schema
- #1418: B2B data sharing obligations for IoT and connected product data
- #1419: EU Data Act vs. GDPR dual-compliance architecture
- #1420: EU Data Act compliance finale — complete SaaS developer toolkit 2026
Key Takeaways
- EU Data Act cloud switching obligations are in force now (since September 2025) — not a future deadline
- The 30-day export deadline, machine-readable format requirement, and no-degradation rule are already enforceable
- Build two distinct APIs: GDPR portability (personal data, individual auth) and Data Act switching (all data, account admin auth)
- The fee prohibition timeline is January 12, 2027 (cost-only) and September 12, 2027 (fully free) — start building fee-free export infrastructure now
- SWIPO code of conduct adherence is the practical compliance benchmark for export format interoperability
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.