Inside the Integration-Native Architecture: Platform, Providers, and Unified Data
How do you make Gmail, Outlook, and Yahoo Mail all speak the same language? A deep dive into the technical architecture that makes native integration possible.
Inside the Integration-Native Architecture: Platform, Providers, and Unified Data
Most integration problems are solved the same way: build an adapter for each service, map their data to your format, and maintain the translation layer forever.
This works, but it scales poorly. Two services require two adapters. Ten services require ten adapters. A hundred services require... well, you see the problem.
Integration-native architecture solves this differently. Instead of building adapters between services, we build platforms that define universal data structures, and providers that conform to those structures.
The difference is subtle but profound. Let me show you how it works.
The Problem: Babel Tower of APIs
Let's start with a concrete example: email.
Gmail's API
// Get a message from Gmail
const response = await gmail.users.messages.get({
userId: 'me',
id: messageId,
format: 'full'
});
// Gmail's data structure
{
id: '18d1e7f3a5c9b2d1',
threadId: '18d1e7f3a5c9b2d1',
labelIds: ['INBOX', 'UNREAD'],
payload: {
headers: [
{ name: 'From', value: 'Alice <[email protected]>' },
{ name: 'Subject', value: 'Q1 Planning' },
{ name: 'Date', value: 'Thu, 1 Feb 2024 09:23:45 -0800' }
],
body: {
data: 'SGVsbG8gV29ybGQ=' // Base64 encoded
},
parts: [ /* attachments */ ]
},
internalDate: '1706806425000'
}
Outlook's API
// Get a message from Outlook
const response = await client
.api(`/me/messages/${messageId}`)
.get();
// Outlook's data structure
{
id: 'AAMkAGI2T...',
conversationId: 'AAQkAGI2...',
subject: 'Q1 Planning',
from: {
emailAddress: {
name: 'Alice',
address: '[email protected]'
}
},
receivedDateTime: '2024-02-01T09:23:45Z',
body: {
contentType: 'HTML',
content: '<html>Hello World</html>'
},
hasAttachments: false,
isRead: false
}
Same concept (an email message), completely different structure:
- Gmail has
payload.headers, Outlook has direct properties - Gmail has
labelIds, Outlook hascategories - Gmail uses
internalDate, Outlook usesreceivedDateTime - Gmail base64-encodes the body, Outlook provides HTML directly
If you want to build an application that works with both, you need to:
- Write separate code for each API
- Transform each provider's data to a common format
- Maintain both implementations as APIs change
- Handle edge cases and inconsistencies
- Repeat for every new provider
This is the adapter pattern, and it's how most integrations work today.
The Alternative: Platform-Provider Architecture
Integration-native architecture inverts this model.
Instead of building adapters from each provider to your application, you build:
- A platform that defines the universal data structure
- Provider implementations that conform to the platform
- Applications that interact only with the platform
The Mail Platform
The Mail platform defines what "email" means universally:
// Universal message structure
interface Message {
id: string;
threadId: string;
subject: string;
from: EmailAddress;
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
timestamp: Date;
body: {
text: string;
html?: string;
};
attachments: Attachment[];
labels: string[];
isRead: boolean;
isStarred: boolean;
}
interface EmailAddress {
name?: string;
address: string;
}
interface Attachment {
id: string;
filename: string;
mimeType: string;
size: number;
url: string;
}
This isn't Gmail's structure or Outlook's structure. It's an abstraction that represents the essence of email, independent of any specific provider.
Provider Implementation
Each provider implements the platform's interface:
interface MailProvider {
// Required methods that every provider must implement
getMessage(id: string): Promise<Message>;
getThread(id: string): Promise<Message[]>;
sendMessage(message: OutgoingMessage): Promise<Message>;
searchMessages(query: SearchQuery): Promise<Message[]>;
updateMessage(id: string, updates: MessageUpdates): Promise<Message>;
// ... other required operations
}
The Gmail provider implements this interface:
class GmailProvider implements MailProvider {
async getMessage(id: string): Promise<Message> {
// Call Gmail API
const gmailMessage = await this.gmailClient.users.messages.get({
userId: 'me',
id: id,
format: 'full'
});
// Transform to platform structure
return this.transformGmailMessage(gmailMessage);
}
private transformGmailMessage(gmailMessage: any): Message {
const headers = gmailMessage.payload.headers;
return {
id: gmailMessage.id,
threadId: gmailMessage.threadId,
subject: this.getHeader(headers, 'Subject'),
from: this.parseEmailAddress(this.getHeader(headers, 'From')),
to: this.parseEmailAddresses(this.getHeader(headers, 'To')),
cc: this.parseEmailAddresses(this.getHeader(headers, 'Cc')),
timestamp: new Date(parseInt(gmailMessage.internalDate)),
body: {
text: this.decodeBody(gmailMessage.payload.body),
html: this.getHtmlBody(gmailMessage.payload)
},
attachments: this.extractAttachments(gmailMessage.payload),
labels: gmailMessage.labelIds || [],
isRead: !gmailMessage.labelIds?.includes('UNREAD'),
isStarred: gmailMessage.labelIds?.includes('STARRED')
};
}
}
The Outlook provider implements the same interface:
class OutlookProvider implements MailProvider {
async getMessage(id: string): Promise<Message> {
// Call Outlook API
const outlookMessage = await this.outlookClient
.api(`/me/messages/${id}`)
.get();
// Transform to platform structure
return this.transformOutlookMessage(outlookMessage);
}
private transformOutlookMessage(outlookMessage: any): Message {
return {
id: outlookMessage.id,
threadId: outlookMessage.conversationId,
subject: outlookMessage.subject,
from: {
name: outlookMessage.from.emailAddress.name,
address: outlookMessage.from.emailAddress.address
},
to: outlookMessage.toRecipients.map(r => ({
name: r.emailAddress.name,
address: r.emailAddress.address
})),
cc: outlookMessage.ccRecipients?.map(r => ({
name: r.emailAddress.name,
address: r.emailAddress.address
})),
timestamp: new Date(outlookMessage.receivedDateTime),
body: {
text: this.htmlToText(outlookMessage.body.content),
html: outlookMessage.body.content
},
attachments: this.transformAttachments(outlookMessage.attachments),
labels: outlookMessage.categories || [],
isRead: outlookMessage.isRead,
isStarred: outlookMessage.flag?.flagStatus === 'flagged'
};
}
}
Using the Platform
Applications interact with the platform, not specific providers:
// Get the Mail platform
const mail = workspace.getPlatform('mail');
// Get a message - works with any provider (Gmail, Outlook, etc.)
const message = await mail.getMessage(messageId);
// The data structure is always the same
console.log(message.subject);
console.log(message.from.address);
console.log(message.body.text);
// Send a message - works with any provider
await mail.sendMessage({
to: [{ address: '[email protected]' }],
subject: 'Re: Q1 Planning',
body: {
text: 'Sounds good! Let\'s schedule a meeting.'
}
});
The key insight: Your application code doesn't know or care whether it's talking to Gmail or Outlook. It just talks to the Mail platform.
The Multi-Provider Reality
Most users don't use just one email provider. They might have:
- [email protected] (Google Workspace)
- [email protected] (Personal Gmail)
- [email protected] (Microsoft 365)
Traditional integrations handle this poorly. You either:
- Pick one provider and force everyone to use it
- Build separate integrations for each and let users choose one
- Build a complex account-switching system
Integration-native architecture handles this naturally:
// Get all connected mail accounts
const accounts = workspace.getAccounts('mail');
// [
// { id: 'acct_1', provider: 'gmail', email: '[email protected]' },
// { id: 'acct_2', provider: 'gmail', email: '[email protected]' },
// { id: 'acct_3', provider: 'outlook', email: '[email protected]' }
// ]
// Search across ALL accounts
const messages = await mail.searchMessages({
query: 'from:[email protected]',
accounts: 'all' // or specify specific account IDs
});
// Results unified across Gmail and Outlook
messages.forEach(msg => {
console.log(`${msg.subject} (from ${msg.accountId})`);
});
The platform handles querying multiple providers in parallel, normalizing results, and presenting them in a unified format.
Beyond Email: The Platform Ecosystem
The same pattern applies to every data domain.
Calendar Platform
interface Event {
id: string;
title: string;
description?: string;
start: Date;
end: Date;
location?: Location;
attendees: Attendee[];
organizer: Attendee;
recurrence?: RecurrenceRule;
status: 'confirmed' | 'tentative' | 'cancelled';
}
interface CalendarProvider {
getEvent(id: string): Promise<Event>;
createEvent(event: NewEvent): Promise<Event>;
updateEvent(id: string, updates: EventUpdates): Promise<Event>;
searchEvents(query: EventQuery): Promise<Event[]>;
// ...
}
Providers: Google Calendar, Outlook Calendar, Apple Calendar, CalDAV
Contacts Platform
interface Contact {
id: string;
name: Name;
emails: EmailAddress[];
phones: PhoneNumber[];
addresses: Address[];
organization?: Organization;
birthday?: Date;
notes?: string;
groups: string[];
}
interface ContactsProvider {
getContact(id: string): Promise<Contact>;
createContact(contact: NewContact): Promise<Contact>;
searchContacts(query: string): Promise<Contact[]>;
// ...
}
Providers: Google Contacts, Outlook Contacts, iCloud Contacts, CardDAV
Files Platform
interface File {
id: string;
name: string;
mimeType: string;
size: number;
path: string;
createdAt: Date;
modifiedAt: Date;
createdBy: User;
permissions: Permission[];
version: number;
}
interface FilesProvider {
getFile(id: string): Promise<File>;
uploadFile(file: FileUpload): Promise<File>;
searchFiles(query: string): Promise<File[]>;
shareFile(id: string, permissions: Permission[]): Promise<File>;
// ...
}
Providers: Google Drive, Dropbox, Box, OneDrive, SharePoint
The Power of Unified Data
When every provider conforms to the same structure, you can do things that are impossible with traditional integrations.
Cross-Platform Search
// Search for "Q1 Planning" across emails, calendar, and files
const results = await workspace.search('Q1 Planning', {
platforms: ['mail', 'calendar', 'files']
});
// Returns unified results
results.forEach(result => {
switch (result.platform) {
case 'mail':
console.log(`Email: ${result.data.subject}`);
break;
case 'calendar':
console.log(`Event: ${result.data.title} at ${result.data.start}`);
break;
case 'files':
console.log(`File: ${result.data.name} (${result.data.size} bytes)`);
break;
}
});
Cross-Platform Relationships
// Get all data related to a specific person
const person = await contacts.getContact(contactId);
// Find related data across platforms
const related = await workspace.getRelatedData(person.emails[0].address);
// Returns
{
emails: Message[], // from Mail platform
events: Event[], // from Calendar platform
files: File[], // from Files platform (shared with this person)
tasks: Task[], // from Tasks platform (assigned to this person)
}
Cross-Platform Workflows
// When an important email arrives, create a task and calendar event
mail.onMessage((message) => {
if (message.from.address === '[email protected]') {
// Create task in Tasks platform
tasks.createTask({
title: `Follow up: ${message.subject}`,
description: message.body.text,
dueDate: addDays(new Date(), 2)
});
// Create calendar event in Calendar platform
calendar.createEvent({
title: `Review: ${message.subject}`,
start: addHours(new Date(), 1),
end: addHours(new Date(), 1.5),
description: `Email: ${message.id}`
});
}
});
This workflow works regardless of which email, task, or calendar provider the user has connected.
Handling Provider-Specific Features
The unified data structure handles the 80% case—the features common across all providers. But what about provider-specific features?
Granular Metadata Access
The platform structure is the default, but you can access provider-specific metadata:
const message = await mail.getMessage(id);
// Standard fields work for all providers
console.log(message.subject);
console.log(message.from);
// Access Gmail-specific features
if (message.provider === 'gmail') {
console.log('Gmail Labels:', message.metadata.gmail.labels);
console.log('Thread ID:', message.metadata.gmail.threadId);
console.log('Snippet:', message.metadata.gmail.snippet);
}
// Access Outlook-specific features
if (message.provider === 'outlook') {
console.log('Categories:', message.metadata.outlook.categories);
console.log('Flag Status:', message.metadata.outlook.flag);
console.log('Importance:', message.metadata.outlook.importance);
}
This provides an escape hatch for advanced use cases while maintaining the unified structure for common operations.
Conditional Features
// Check if provider supports specific features
if (mail.supportsFeature('labels')) {
// Gmail supports labels
await mail.addLabel(messageId, 'Important');
} else if (mail.supportsFeature('categories')) {
// Outlook supports categories
await mail.addCategory(messageId, 'Important');
}
The Technical Implementation
How does this work under the hood?
Provider Registry
class PlatformRegistry {
private providers = new Map<string, Map<string, Provider>>();
registerProvider(platform: string, provider: Provider) {
if (!this.providers.has(platform)) {
this.providers.set(platform, new Map());
}
this.providers.get(platform)!.set(provider.id, provider);
}
getProvider(platform: string, accountId: string): Provider {
const account = this.getAccount(accountId);
return this.providers.get(platform)?.get(account.providerId);
}
}
// Register providers
registry.registerProvider('mail', new GmailProvider());
registry.registerProvider('mail', new OutlookProvider());
registry.registerProvider('calendar', new GoogleCalendarProvider());
registry.registerProvider('calendar', new OutlookCalendarProvider());
Platform API
class Platform {
constructor(
private name: string,
private registry: PlatformRegistry
) {}
async getMessage(id: string, accountId?: string): Promise<Message> {
// Determine which account to use
const account = accountId
? this.getAccount(accountId)
: this.getDefaultAccount(this.name);
// Get the appropriate provider
const provider = this.registry.getProvider(this.name, account.id);
// Call provider's implementation
const message = await provider.getMessage(id);
// Add metadata
message.accountId = account.id;
message.provider = account.providerId;
return message;
}
async searchMessages(
query: SearchQuery,
accounts: 'all' | string[] = 'all'
): Promise<Message[]> {
// Determine which accounts to search
const accountsToSearch = accounts === 'all'
? this.getAllAccounts(this.name)
: accounts.map(id => this.getAccount(id));
// Search in parallel across all accounts
const results = await Promise.all(
accountsToSearch.map(account => {
const provider = this.registry.getProvider(this.name, account.id);
return provider.searchMessages(query);
})
);
// Flatten and sort results
return results
.flat()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
}
Caching and Performance
class CachedPlatform extends Platform {
private cache = new LRUCache<string, any>({ max: 1000 });
async getMessage(id: string, accountId?: string): Promise<Message> {
const cacheKey = `${accountId}:${id}`;
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
// Call provider
const message = await super.getMessage(id, accountId);
// Cache result
this.cache.set(cacheKey, message);
return message;
}
// Invalidate cache when data changes
async sendMessage(message: OutgoingMessage): Promise<Message> {
const sent = await super.sendMessage(message);
// Invalidate relevant cache entries
this.invalidateCache(sent.threadId);
return sent;
}
}
The Competitive Moat
This architecture creates several defensible advantages:
1. Provider additions are exponential in value
Each new provider:
- Works with all existing platforms
- Works with all applications built on the platforms
- Requires no changes to existing code
2. Network effects across platforms
The more platforms that exist:
- The more data can be unified
- The more powerful cross-platform features become
- The more valuable the ecosystem
3. Application development becomes platform composition
Building a new application means composing platforms, not writing integration code:
// Build a sales CRM in ~100 lines
function SalesCRM() {
const mail = usePlatform('mail');
const calendar = usePlatform('calendar');
const contacts = usePlatform('contacts');
const files = usePlatform('files');
// Your UI that uses unified data from all platforms
return <DealPipeline
emails={mail}
events={calendar}
contacts={contacts}
files={files}
/>;
}
4. Data portability becomes trivial
Switching from Gmail to Outlook? Just connect the new account. Everything continues working.
Company acquisition requires merging different email systems? Both work simultaneously.
The Future
Integration-native architecture enables capabilities that are impossible with traditional integrations:
AI that understands your entire workspace:
// AI assistant with access to all platforms
const assistant = workspace.getAssistant();
await assistant.ask(
"What did Alice say about the Q1 planning meeting?"
);
// Searches across emails, calendar, files, and chat
// Returns unified response with sources
Automated workflows across platforms:
// When a deal is marked "won" in CRM
crm.onDealWon(async (deal) => {
// Create implementation project in project management
await projects.createProject({
name: `Implementation: ${deal.company}`,
team: deal.team,
dueDate: addMonths(deal.closeDate, 3)
});
// Schedule kickoff meeting
await calendar.createEvent({
title: `Kickoff: ${deal.company}`,
attendees: [...deal.team, ...deal.contacts],
start: addDays(deal.closeDate, 7)
});
// Create shared folder
await files.createFolder({
name: deal.company,
permissions: deal.team
});
// Send welcome email
await mail.sendMessage({
to: deal.contacts,
subject: `Welcome to ${workspace.name}!`,
body: templates.render('customer-welcome', { deal })
});
});
Custom interfaces for every vertical:
Different industries can build completely custom interfaces on the same platform foundation:
- Healthcare: EHR + scheduling + billing integrated natively
- Real estate: MLS + transaction + marketing integrated natively
- Financial advisory: Portfolio + planning + CRM integrated natively
Each built in weeks instead of years, on a foundation that handles all the integration complexity.
Conclusion
Integration-native architecture isn't just better integration. It's a fundamentally different approach to building software.
Instead of applications that connect through integrations, you have:
- Platforms that define universal data structures
- Providers that conform to those structures
- Applications that compose platforms
The result:
- Faster development (build features, not integrations)
- Better reliability (no synchronization delays or mapping errors)
- True customization (build exactly what you need)
- Provider independence (switch providers without breaking applications)
This is the architecture that enables every company to have software built specifically for how they work, not how generic tools force them to work.
The future of business software isn't better integration. It's native integration from the foundation up.
Want to build on integration-native architecture? Explore how Spatio's platform enables developers to build custom workspaces without integration overhead. Explore Spatio →