Back to Blog
Engineering

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.

Matthew Park
February 2, 2024
12 min read

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 has categories
  • Gmail uses internalDate, Outlook uses receivedDateTime
  • Gmail base64-encodes the body, Outlook provides HTML directly

If you want to build an application that works with both, you need to:

  1. Write separate code for each API
  2. Transform each provider's data to a common format
  3. Maintain both implementations as APIs change
  4. Handle edge cases and inconsistencies
  5. 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:

  1. A platform that defines the universal data structure
  2. Provider implementations that conform to the platform
  3. 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:

Traditional integrations handle this poorly. You either:

  1. Pick one provider and force everyone to use it
  2. Build separate integrations for each and let users choose one
  3. 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 →

Tags

ArchitectureTechnicalPlatform DesignEngineering

Related Articles

Enjoyed this article?

Subscribe to our newsletter for more insights and updates