Last updated

Fluid Platform Mobile Widget Implementation Guide

Overview

Mobile widgets on the Fluid platform allow you to create custom functionality that can be integrated into mobile apps through embedded web views. These widgets are web applications that maintain secure authentication and access to company data while being displayed within the mobile app interface.

Architecture Overview

Widget Components

  • Standalone Web Application: Each widget is hosted at its own URL
  • Authentication Layer: Secure token-based authentication via Fluid's auth token system
  • Data Integration: Connection to backend systems (your existing databases, APIs, etc.)
  • Mobile App Integration: Embedded via web views in mobile applications

Key Concepts

  • Mobile Widgets: Custom components for mobile app integration via MobileWidget model
  • Authentication Tokens: Secure access tokens generated by UserCompany#generate_auth_token
  • API Access: Widgets can access both Fluid APIs and your own backend APIs
  • User Identification: Using external_id from UserCompany as the primary identifier for business operations

Implementation Steps

Step 1: Widget Planning

Before implementation, consider:

  • Widget Purpose: KPIs, volumes, bonus tracking, team performance, etc.
  • Data Sources: Determine if data comes from Fluid or your existing systems
  • Update Frequency: Real-time vs. periodic updates
  • Required User Fields: Ensure users have external_id for business operations

Step 2: Create the Widget Application

Basic HTML Structure

<!DOCTYPE html>
<html>
<head>
    <title>Widget Name</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        /* Mobile-optimized styles */
        body {
            margin: 0;
            padding: 10px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }
        .loading {
            text-align: center;
            padding: 20px;
        }
        .error {
            color: #e74c3c;
            padding: 20px;
            text-align: center;
        }
        .widget-content {
            padding: 15px;
        }
        .metrics {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 15px;
            margin-top: 20px;
        }
        .metric {
            background: #f5f5f5;
            padding: 15px;
            border-radius: 8px;
            text-align: center;
        }
        .label {
            display: block;
            font-size: 12px;
            color: #666;
            margin-bottom: 5px;
        }
        .value {
            display: block;
            font-size: 20px;
            font-weight: bold;
            color: #333;
        }
    </style>
</head>
<body>
    <div id="widget-container">
        <div class="loading">Loading...</div>
    </div>
    <script src="widget.js"></script>
</body>
</html>

JavaScript Implementation

// widget.js
class FluidWidget {
    constructor() {
        this.authToken = this.getAuthToken();
        this.fluidApiUrl = 'https://your-fluid-api.com/api'; // Configure this
        this.backendApiUrl = 'https://your-backend.com/api'; // Your backend
        this.init();
    }

    getAuthToken() {
        // Extract auth token from URL parameters
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('auth_token');
    }

    async init() {
        if (!this.authToken) {
            this.showError('No authentication token provided');
            return;
        }

        try {
            // Validate token with Fluid and get user info
            const fluidUser = await this.getUserFromFluid();
            
            if (!fluidUser) {
                this.showError('Failed to authenticate with Fluid');
                return;
            }

            // Validate user has required fields
            if (!fluidUser.externalId) {
                this.showError('User account not properly configured (missing external ID)');
                return;
            }
            
            // Fetch widget-specific data from your backend using external_id
            const widgetData = await this.fetchWidgetData(fluidUser);
            
            // Render the widget
            this.render(widgetData, fluidUser);
        } catch (error) {
            this.showError('Failed to load widget data');
            console.error('Widget error:', error);
        }
    }

    async getUserFromFluid() {
        try {
            // Use the auth token in the URL path - the API accepts either user_company.id or auth_token.token
            // The auth_token is also passed as a Bearer token for authentication
            const response = await fetch(`${this.fluidApiUrl}/v2025-06/users/${this.authToken}`, {
                headers: {
                    'Authorization': `Bearer ${this.authToken}`,
                    'Content-Type': 'application/json'
                }
            });

            if (!response.ok) {
                console.error('Fluid authentication failed:', response.status);
                return null;
            }

            const userData = await response.json();
            
            // Map response to expected structure based on UserCompanyBlueprinter
            const userCompany = userData.user_company || userData;
            return {
                id: userCompany.id,
                externalId: userCompany.external_id,
                companyId: userCompany.company_id,
                email: userCompany.email,
                username: userCompany.username,
                roles: userCompany.roles,
                metadata: userCompany.metadata || {}
            };
        } catch (error) {
            console.error('Error calling Fluid API:', error);
            return null;
        }
    }

    async fetchWidgetData(fluidUser) {
        // Use external_id as the primary identifier for your business logic
        const response = await fetch(
            `${this.backendApiUrl}/widget-data?external_id=${encodeURIComponent(fluidUser.externalId)}`, 
            {
                headers: {
                    'Authorization': `Bearer ${this.authToken}`,
                    'X-Company-Id': fluidUser.companyId || '',
                    'Content-Type': 'application/json'
                }
            }
        );

        if (!response.ok) {
            throw new Error('Failed to fetch widget data');
        }

        return await response.json();
    }

    render(data, fluidUser) {
        const container = document.getElementById('widget-container');
        container.innerHTML = `
            <div class="widget-content">
                <h2>Welcome, ${data.userName || fluidUser.username}</h2>
                <div class="user-info">
                    <small>ID: ${fluidUser.externalId} | Company: ${fluidUser.companyId}</small>
                </div>
                <div class="metrics">
                    <div class="metric">
                        <span class="label">Personal Volume</span>
                        <span class="value">$${(data.personalVolume || 0).toLocaleString()}</span>
                    </div>
                    <div class="metric">
                        <span class="label">Team Volume</span>
                        <span class="value">$${(data.teamVolume || 0).toLocaleString()}</span>
                    </div>
                    <div class="metric">
                        <span class="label">Current Rank</span>
                        <span class="value">${data.rank || 'N/A'}</span>
                    </div>
                    <div class="metric">
                        <span class="label">New Enrollments</span>
                        <span class="value">${data.newEnrollments || 0}</span>
                    </div>
                </div>
            </div>
        `;
    }

    showError(message) {
        const container = document.getElementById('widget-container');
        container.innerHTML = `<div class="error">${message}</div>`;
    }
}

// Initialize widget when page loads
document.addEventListener('DOMContentLoaded', () => {
    new FluidWidget();
});

Step 3: Backend API Implementation

Node.js/Express Example

const express = require('express');
const axios = require('axios');
const app = express();

// Configuration
const FLUID_API_URL = process.env.FLUID_API_URL || 'https://your-fluid-api.com/api';

// Middleware to validate Fluid auth token and get user info
async function validateFluidToken(req, res, next) {
    const authToken = req.headers.authorization?.replace('Bearer ', '');
    
    if (!authToken) {
        return res.status(401).json({ error: 'No auth token provided' });
    }

        try {
            // Validate token with Fluid API using auth token in URL path
            const response = await axios.get(`${FLUID_API_URL}/v2025-06/users/${authToken}`, {
                headers: {
                    'Authorization': `Bearer ${authToken}`,
                    'Content-Type': 'application/json'
                }
            });

            const fluidUser = response.data.user_company || response.data;
        
        // Validate user has required fields
        if (!fluidUser.external_id) {
            return res.status(400).json({ 
                error: 'User missing external_id - cannot proceed with business operations' 
            });
        }

        // Attach user to request object
        req.fluidUser = {
            id: fluidUser.id,
            externalId: fluidUser.external_id,
            companyId: fluidUser.company_id,
            email: fluidUser.email,
            username: fluidUser.username,
            roles: fluidUser.roles,
            metadata: fluidUser.metadata || {}
        };
        
        next();
    } catch (error) {
        if (error.response?.status === 401) {
            return res.status(401).json({ error: 'Invalid or expired token' });
        } else if (error.response?.status === 404) {
            return res.status(404).json({ error: 'User not found for this token' });
        }
        console.error('Fluid API error:', error.message);
        res.status(500).json({ error: 'Authentication error' });
    }
}

// Widget data endpoint
app.get('/api/widget-data', validateFluidToken, async (req, res) => {
    const externalId = req.query.external_id || req.fluidUser.externalId;
    const companyId = req.headers['x-company-id'] || req.fluidUser.companyId;
    
    try {
        // Use external_id as the primary identifier for your business logic
        const widgetData = await getWidgetDataFromDatabase(externalId, companyId);
        res.json(widgetData);
    } catch (error) {
        console.error('Error fetching widget data:', error);
        res.status(500).json({ error: 'Failed to fetch data' });
    }
});

async function getWidgetDataFromDatabase(externalId, companyId) {
    // This would connect to your existing database/systems
    // Use external_id as customer_id or rep_id in your system
    
    // Example query to your database:
    // const userData = await db.query(
    //     'SELECT * FROM users WHERE external_id = ? AND company_id = ?',
    //     [externalId, companyId]
    // );
    
    return {
        userName: 'John Doe',
        personalVolume: 5420.50,
        teamVolume: 45230.75,
        rank: 'Gold',
        newEnrollments: 3,
        monthlyGoal: 10000,
        goalProgress: 54.2
    };
}

// CORS configuration (if needed)
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Company-Id');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    next();
});

app.listen(3000, () => {
    console.log('Widget backend running on port 3000');
});

Ruby on Rails Backend Example

class Api::WidgetController < ApplicationController
  before_action :validate_fluid_token
  
  def widget_data
    external_id = params[:external_id] || @fluid_user[:external_id]
    company_id = request.headers['X-Company-Id'] || @fluid_user[:company_id]
    
    # Use external_id for business operations
    widget_data = get_widget_data_from_database(external_id, company_id)
    
    render json: widget_data
  rescue StandardError => e
    Rails.logger.error "Error fetching widget data: #{e.message}"
    render json: { error: 'Failed to fetch data' }, status: :internal_server_error
  end

  private

  def validate_fluid_token
    auth_token = request.headers['Authorization']&.gsub(/^Bearer /, '')
    
    return render json: { error: 'No auth token provided' }, status: :unauthorized if auth_token.blank?

    begin
      response = HTTParty.get(
        "#{ENV['FLUID_API_URL']}/api/v2025-06/users/#{auth_token}",
        headers: {
          'Authorization' => "Bearer #{auth_token}",
          'Content-Type' => 'application/json'
        }
      )

      if response.success?
        fluid_user = response.parsed_response['user_company'] || response.parsed_response
        
        if fluid_user['external_id'].blank?
          return render json: { error: 'User missing external_id - cannot proceed' }, status: :bad_request
        end

        @fluid_user = {
          id: fluid_user['id'],
          external_id: fluid_user['external_id'],
          company_id: fluid_user['company_id'],
          email: fluid_user['email'],
          username: fluid_user['username'],
          roles: fluid_user['roles']
        }
      else
        render json: { error: 'Invalid or expired token' }, status: :unauthorized
      end
    rescue StandardError => e
      Rails.logger.error "Fluid API error: #{e.message}"
      render json: { error: 'Authentication error' }, status: :internal_server_error
    end
  end

  def get_widget_data_from_database(external_id, company_id)
    # Connect to your existing database/systems using external_id
    {
      userName: 'John Doe',
      personalVolume: 5420.50,
      teamVolume: 45230.75,
      rank: 'Gold',
      newEnrollments: 3,
      monthlyGoal: 10000,
      goalProgress: 54.2
    }
  end
end

Step 4: Authentication Flow Details

How Fluid Generates Auth Tokens

Based on the codebase analysis, here's how authentication works:

  1. Token Generation: UserCompany#generate_auth_token creates an AuthToken record
  2. Token Structure: Auth tokens are stored in the auth_tokens table, linked to user_companies
  3. Token Validation: Fluid validates tokens through UserCompany.find_with_active_auth_token(token)
  4. Mobile Widget Integration: The mobile app automatically appends the auth token to widget URLs

Token Validation Process

# In Fluid's codebase (CommonEntityConcern)
def handle_user_company_token(token)
  user_company = UserCompany.active.find_by(token: token) || 
                 UserCompany.active.find_with_active_auth_token(token)
  return unless user_company

  # Set current entities for the request
  assign_current_user(user_company.user)
  assign_current_company(user_company.company)
  assign_current_affiliate(user_company)
  
  user_company
end

Step 5: Common Backend Endpoints

These endpoints should be created in YOUR backend system (not Fluid's):

// KPI Dashboard Data
GET /api/widgets/kpi-dashboard?external_id={external_id}
Headers: Authorization: Bearer {token}
Response: {
    personalVolume: 5420.50,
    teamVolume: 45230.75,
    monthlyGoal: 10000,
    goalProgress: 54.2,
    rank: "Gold",
    nextRankProgress: 75
}

// Team Performance Data
GET /api/widgets/team-performance?external_id={external_id}
Headers: Authorization: Bearer {token}
Response: {
    directTeamCount: 12,
    totalTeamSize: 156,
    activeMembers: 89,
    newMembersThisMonth: 5,
    topPerformers: [
        { name: "Jane Doe", volume: 12450.00, rank: "Silver" },
        { name: "Bob Smith", volume: 8930.50, rank: "Bronze" }
    ]
}

// Bonus Tracking Information
GET /api/widgets/bonus-tracker?external_id={external_id}
Headers: Authorization: Bearer {token}
Response: {
    currentBonuses: [
        { name: "Fast Start Bonus", amount: 500, qualified: true },
        { name: "Leadership Bonus", amount: 1200, qualified: false }
    ],
    projectedEarnings: 2340.00,
    qualifiedBonuses: 3,
    pendingQualifications: 2
}

Step 6: Mobile App Integration

Widget URL Format

https://your-backend.com/widgets/{widget-name}

Note: The auth_token is appended by Fluid automatically when the widget is loaded in the mobile app.

Mobile Widget Configuration

Based on the codebase, widgets are configured through the MobileWidget model:

# Example mobile widget configuration
mobile_widget = MobileWidget.create!(
  company: company,
  name: "KPI Dashboard",
  embed_url: "https://your-backend.com/widgets/kpi-dashboard",
  active: true
)

The mobile app will:

  1. Fetch widget configuration from Fluid via /api/mobile_widgets/:id
  2. Automatically append authentication token to the embed URL
  3. Load widget in a WebView component
  4. Handle navigation and refresh events

Step 7: Common Widget Types

1. KPI Dashboard Widget

class KPIDashboard extends FluidWidget {
    render(data, fluidUser) {
        const container = document.getElementById('widget-container');
        container.innerHTML = `
            <div class="kpi-dashboard">
                <div class="user-header">
                    <h2>${fluidUser.username}</h2>
                    <p>Customer ID: ${fluidUser.externalId}</p>
                </div>
                <div class="kpi-grid">
                    <div class="kpi-card">
                        <div class="kpi-value">$${data.personalVolume.toLocaleString()}</div>
                        <div class="kpi-label">Personal Volume</div>
                        <div class="kpi-progress">
                            <div class="progress-bar" style="width: ${data.personalProgress}%"></div>
                        </div>
                    </div>
                    <div class="kpi-card">
                        <div class="kpi-value">$${data.teamVolume.toLocaleString()}</div>
                        <div class="kpi-label">Team Volume</div>
                    </div>
                    <div class="kpi-card">
                        <div class="kpi-value">${data.newEnrollments}</div>
                        <div class="kpi-label">New Enrollments</div>
                    </div>
                    <div class="kpi-card">
                        <div class="kpi-value">${data.rank}</div>
                        <div class="kpi-label">Current Rank</div>
                    </div>
                </div>
            </div>
        `;
    }
}

2. Team Performance Widget

class TeamPerformance extends FluidWidget {
    async fetchWidgetData(fluidUser) {
        const response = await fetch(
            `${this.backendApiUrl}/widgets/team-performance?external_id=${fluidUser.externalId}`,
            {
                headers: {
                    'Authorization': `Bearer ${this.authToken}`,
                    'X-Company-Id': fluidUser.companyId
                }
            }
        );
        
        if (!response.ok) {
            throw new Error('Failed to fetch team data');
        }
        
        return await response.json();
    }
    
    render(data, fluidUser) {
        const container = document.getElementById('widget-container');
        const teamHtml = data.topPerformers.map(member => `
            <div class="team-member">
                <div class="member-info">
                    <span class="member-name">${member.name}</span>
                    <span class="member-rank">${member.rank}</span>
                </div>
                <span class="member-volume">$${member.volume.toLocaleString()}</span>
            </div>
        `).join('');

        container.innerHTML = `
            <div class="team-widget">
                <div class="team-summary">
                    <h3>Team Overview</h3>
                    <div class="stats-grid">
                        <div class="stat">
                            <span class="stat-value">${data.directTeamCount}</span>
                            <span class="stat-label">Direct Team</span>
                        </div>
                        <div class="stat">
                            <span class="stat-value">${data.totalTeamSize}</span>
                            <span class="stat-label">Total Team</span>
                        </div>
                        <div class="stat">
                            <span class="stat-value">${data.activeMembers}</span>
                            <span class="stat-label">Active</span>
                        </div>
                    </div>
                </div>
                <div class="top-performers">
                    <h3>Top Performers</h3>
                    ${teamHtml}
                </div>
            </div>
        `;
    }
}

Best Practices

Authentication & Security

  • Always validate tokens server-side: Never trust client-side validation alone
  • Use HTTPS everywhere: All communications must be encrypted
  • Handle token expiration: Gracefully handle expired tokens with clear user feedback
  • Validate user fields: Always check for required fields like external_id
  • Use external_id consistently: This should be your primary user identifier for business logic

Performance

  • Implement caching: Cache frequently accessed data appropriately
  • Lazy loading: Load data progressively for better perceived performance
  • Batch API calls: Combine multiple requests where possible
  • Optimize payloads: Keep data transfers minimal for mobile networks
  • Error retry logic: Implement smart retry for failed requests

User Experience

  • Responsive design: Test on all device sizes
  • Loading states: Always show loading indicators
  • Error messages: Provide clear, actionable error messages
  • Offline handling: Consider offline scenarios for mobile users
  • Touch-friendly: Ensure all interactive elements are easily tappable

Development Workflow

  1. Local Development

    • Use ngrok or similar for testing with real tokens
    • Mock Fluid API responses for faster development
  2. Testing

    • Test with real Fluid tokens
    • Verify external_id mapping
    • Test token expiration scenarios
    • Test with users missing required fields
  3. Deployment

    • Ensure all endpoints use HTTPS
    • Set up proper logging and monitoring
    • Configure CORS if needed
    • Document API endpoints for mobile team

Configuration in Fluid

Widget Registration

Register widgets in Fluid through the admin interface or programmatically:

# Create a mobile widget in Fluid
mobile_widget = MobileWidget.create!(
  company: company,
  name: "KPI Dashboard",
  description: "Real-time performance metrics",
  embed_url: "https://your-backend.com/widgets/kpi-dashboard",
  height: 400,  # Height in pixels (100-1000)
  cover_image_url: "https://example.com/widget-cover.jpg",
  page_scope: {
    home: true,
    explore: false
  },
  active: true
)

Mobile Widget Management API

Fluid provides comprehensive API endpoints for managing mobile widgets including resizing and configuration updates.

Available Endpoints

1. List Mobile Widgets

GET /api/company/mobile_widgets
Authorization: Bearer {company_admin_token}

2. Get Mobile Widget Details

GET /api/company/mobile_widgets/{widget_id}
Authorization: Bearer {company_admin_token}

3. Create Mobile Widget

POST /api/company/mobile_widgets
Authorization: Bearer {company_admin_token}
Content-Type: application/json

{
  "mobile_widget": {
    "name": "KPI Dashboard",
    "embed_url": "https://your-backend.com/widgets/kpi-dashboard",
    "height": 400,
    "cover_image_url": "https://example.com/cover.jpg",
    "page_scope": {
      "home": true,
      "explore": false
    },
    "droplet_installation_id": null
  }
}

4. Update Mobile Widget (Including Resizing)

PUT /api/company/mobile_widgets/{widget_id}
Authorization: Bearer {company_admin_token}
Content-Type: application/json

{
  "mobile_widget": {
    "name": "Updated Widget Name",
    "embed_url": "https://your-backend.com/widgets/updated-url",
    "height": 600,  // Resize widget (100-1000 pixels)
    "cover_image_url": "https://example.com/new-cover.jpg",
    "page_scope": {
      "home": true,
      "explore": true
    }
  }
}

5. Delete Mobile Widget

DELETE /api/company/mobile_widgets/{widget_id}
Authorization: Bearer {company_admin_token}

Widget Properties

Based on the codebase analysis, mobile widgets support the following configurable properties:

Required Properties (for creation)

  • name (string): Display name for the widget
  • embed_url (string): URL of your widget application
  • height (integer): Widget height in pixels (100-1000)
  • cover_image_url (string): Preview image URL for the widget

Optional Properties

  • page_scope (object): Controls where the widget appears
    • home (boolean): Show on home page
    • explore (boolean): Show on explore page
  • droplet_installation_id (integer, nullable): Link to droplet installation if applicable

Widget Sizing and Dimensions

Mobile widgets have flexible sizing options:

Height Constraints

  • Minimum height: 100 pixels
  • Maximum height: 1000 pixels
  • Default behavior: Widget width is responsive to mobile screen size
  • Height control: Explicitly set via the height property

Responsive Behavior

The mobile app handles widget display with these characteristics:

  • Width: Automatically adapts to mobile screen width
  • Height: Fixed based on the height property you specify
  • Overflow: Scrollable if content exceeds specified height
  • Aspect ratio: Maintained within the specified height constraint

Dynamic Widget Resizing

You can programmatically resize widgets using the update API:

// Example: Resize a widget based on content
async function resizeWidget(widgetId, newHeight) {
    const response = await fetch(`/api/company/mobile_widgets/${widgetId}`, {
        method: 'PUT',
        headers: {
            'Authorization': `Bearer ${adminToken}`,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            mobile_widget: {
                height: Math.max(100, Math.min(1000, newHeight)) // Ensure within bounds
            }
        })
    });
    
    if (response.ok) {
        console.log('Widget resized successfully');
    }
}

Page Scope Configuration

Control where your widgets appear in the mobile app:

// Show widget on both home and explore pages
{
  "page_scope": {
    "home": true,
    "explore": true
  }
}

// Show widget only on home page
{
  "page_scope": {
    "home": true,
    "explore": false
  }
}

// Show widget only on explore page
{
  "page_scope": {
    "home": false,
    "explore": true
  }
}

Troubleshooting

Common Issues and Solutions

Authentication Failures

// Check token format
console.log('Token:', authToken?.substring(0, 10) + '...');

// Verify API endpoint
console.log('Calling:', `${FLUID_API_URL}/api/me`);

// Log response status
console.log('Response status:', response.status);

Missing External ID

// Always validate external_id before business operations
if (!fluidUser.externalId) {
    console.error('User missing external_id');
    // Handle appropriately - show error or use fallback
}

CORS Issues

// Backend CORS configuration
app.use(cors({
    origin: '*', // Configure appropriately for production
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Company-Id']
}));

Performance Issues

// Implement caching
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedData(key, fetcher) {
    const cached = cache.get(key);
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
        return cached.data;
    }
    
    const data = await fetcher();
    cache.set(key, { data, timestamp: Date.now() });
    return data;
}

Deployment Checklist

  • All URLs use HTTPS
  • Fluid API endpoint correctly configured
  • Authentication flow thoroughly tested
  • External ID validation implemented
  • Error handling covers all scenarios
  • Mobile-responsive design verified
  • Performance tested with production data volumes
  • CORS properly configured (if needed)
  • Logging and monitoring configured
  • API documentation provided to mobile team
  • Widget registered in Fluid admin
  • Tested in actual mobile app environment
  • Fallback behavior for missing user fields
  • Token expiration handling implemented
  • Security review completed

Support and Resources

  • Fluid API Documentation: Contact Fluid support for API documentation
  • Mobile Integration: Coordinate with mobile app development team
  • Widget Examples: Reference implementations in shared repository
  • Support: Contact Fluid support for platform-specific questions

Key Implementation Notes

  1. Authentication Flow: Fluid automatically appends auth tokens to widget URLs when loaded in the mobile app
  2. Token Validation: Use standard HTTP Bearer token authentication to validate with Fluid's API
  3. External ID Usage: The external_id field in UserCompany is your primary business identifier
  4. Mobile Widget Model: Widgets are configured through Fluid's MobileWidget model
  5. Auth Token Generation: Tokens are generated via UserCompany#generate_auth_token and stored in auth_tokens table

This guide provides a complete foundation for creating mobile widgets on the Fluid platform, validated against the actual codebase implementation.