Skip to content

Tutorial 4: Production Integration Guide

Deploy your components to production with webhooks, monitoring, and best practices.

What You'll Learn

  • Production deployment workflow
  • Webhook configuration and security
  • Error handling and monitoring
  • Performance optimization
  • Versioning strategy

Difficulty: Advanced Time: 60 minutes Prerequisites: All previous tutorials

Prerequisites Checklist

Before deploying to production:

  • [ ] Component tested in sandbox mode
  • [ ] Backend API endpoint ready
  • [ ] API authentication configured
  • [ ] Error handling implemented
  • [ ] Performance tested (< 3s load time)
  • [ ] Component versioned (semantic versioning)

Production Architecture

mermaid
graph TB
    A[User Opens Board] --> B[BoardAPI]
    B --> C{Cached?}
    C -->|Yes| D[Return Cached Data]
    C -->|No| E[Call Webhook]
    E --> F[Your Backend API]
    F --> G[Database/CRM]
    G --> F
    F --> E
    E --> H[Cache Response]
    H --> B
    B --> A

Step 1: Prepare Your Backend

1.1 Create Production Endpoint

Express.js Example:

javascript
// server.js
const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();

// Security middleware
app.use(helmet());
app.use(express.json());

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);

// Authentication middleware
function authenticateApiKey(req, res, next) {
  const apiKey = req.headers['authorization']?.replace('Bearer ', '');

  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }

  // Verify API key against database
  if (!isValidApiKey(apiKey)) {
    return res.status(403).json({ error: 'Invalid API key' });
  }

  next();
}

// Restaurant endpoint (from Tutorial 3)
app.get('/api/restaurant/:slug', authenticateApiKey, async (req, res) => {
  try {
    const { slug } = req.params;

    // Fetch from database
    const restaurant = await db.restaurants.findOne({
      where: { slug },
      include: ['menu_items']
    });

    if (!restaurant) {
      return res.status(404).json({ error: 'Restaurant not found' });
    }

    // Format response
    res.json({
      name: restaurant.name,
      cuisine: restaurant.cuisine,
      logo: restaurant.logo_url,
      menu: restaurant.menu_items.map(item => ({
        id: item.id,
        name: item.name,
        description: item.description,
        price: parseFloat(item.price),
        category: item.category
      }))
    });

  } catch (error) {
    console.error('Error fetching restaurant:', error);
    res.status(500).json({
      error: 'Internal server error',
      message: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`API server running on port ${PORT}`);
});

function isValidApiKey(key) {
  // TODO: Check against database
  return key === process.env.VALID_API_KEY;
}

1.2 Deploy Your Backend

Option A: Heroku

bash
git init
heroku create my-restaurant-api
git push heroku main

Option B: DigitalOcean/VPS

bash
# Install PM2 for process management
npm install -g pm2

# Start with PM2
pm2 start server.js --name restaurant-api
pm2 save
pm2 startup

Option C: Serverless (AWS Lambda)

javascript
// handler.js
exports.handler = async (event) => {
  const slug = event.pathParameters.slug;
  // ... restaurant logic
  return {
    statusCode: 200,
    body: JSON.stringify(restaurantData)
  };
};

1.3 Test Your Backend

bash
# Health check
curl https://api.yourcompany.com/health

# Test endpoint
curl https://api.yourcompany.com/api/restaurant/pizza-heaven \
  -H "Authorization: Bearer your-api-key"

# Expected response:
# {
#   "name": "Pizza Heaven",
#   "cuisine": "Italian",
#   "menu": [...]
# }

Step 2: Configure Data Provider

2.1 Create Production Board

bash
TOKEN=$(curl -X POST https://app.boardapi.io/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"your@email.com","password":"your-password"}' \
  | jq -r '.accessToken')

curl -X POST https://app.boardapi.io/api/v1/boards \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Restaurant Menu - Production",
    "data_provider": {
      "type": "webhook",
      "webhook_url": "https://api.yourcompany.com/api/restaurant/:restaurantSlug",
      "method": "GET",
      "headers": {
        "Authorization": "Bearer YOUR_BACKEND_API_KEY",
        "Content-Type": "application/json"
      },
      "timeout": 5000,
      "retry": {
        "max_attempts": 3,
        "backoff": "exponential"
      },
      "cache": {
        "enabled": true,
        "ttl": 300
      }
    },
    "config": {
      "components": [
        {
          "type": "restaurant-menu",
          "url": "https://app.boardapi.io/components/restaurant-menu@1.0.0/index.html",
          "version": "1.0.0",
          "props": {
            "restaurantSlug": "pizza-heaven"
          }
        }
      ]
    }
  }' | tee production-board.json

2.2 Data Provider Configuration Explained

FieldDescriptionDefault
webhook_urlYour backend API endpointRequired
methodHTTP method (GET/POST)GET
headersAuthentication headers{}
timeoutMax response time (ms)5000
retry.max_attemptsRetry failed requests3
retry.backoffRetry strategyexponential
cache.enabledCache responsesfalse
cache.ttlCache duration (seconds)0

Step 3: Implement Error Handling

3.1 Component-Side Error Handling

Update your component's index.html:

javascript
// Enhanced error handling
async function fetchRestaurantData() {
  const contentEl = document.getElementById('content');

  try {
    // Show loading state
    renderLoading();

    // Check for sandbox data
    if (window.externalData) {
      restaurantData = window.externalData;
      renderMenu();
      return;
    }

    // Request from parent
    window.parent.postMessage({
      type: 'request-external-data',
      component: 'restaurant-menu',
      params: { restaurantSlug: props.restaurantSlug }
    }, '*');

    // Timeout handling
    const timeout = setTimeout(() => {
      if (!restaurantData) {
        renderError({
          type: 'timeout',
          message: 'Request timed out. Please refresh or try again later.',
          recoverable: true
        });
      }
    }, 10000);

    // Listen for response
    window.addEventListener('message', (event) => {
      if (event.data.type === 'external-data-response') {
        clearTimeout(timeout);

        if (event.data.error) {
          renderError({
            type: 'api_error',
            message: event.data.error.message,
            recoverable: true
          });
        } else {
          restaurantData = event.data.data;
          renderMenu();
        }
      }
    });

  } catch (error) {
    console.error('Fatal error:', error);
    renderError({
      type: 'fatal',
      message: 'An unexpected error occurred',
      recoverable: false
    });
  }
}

// Enhanced error rendering
function renderError(error) {
  const html = `
    <div class="error-container">
      <div class="error-icon">⚠️</div>
      <div class="error-message">${error.message}</div>
      ${error.recoverable ? `
        <button onclick="fetchRestaurantData()" class="retry-btn">
          Retry
        </button>
      ` : ''}
    </div>
  `;

  document.getElementById('content').innerHTML = html;

  // Emit error event for monitoring
  window.parent.postMessage({
    type: 'component-event',
    component: 'restaurant-menu',
    event: 'error',
    data: {
      errorType: error.type,
      message: error.message,
      timestamp: new Date().toISOString()
    }
  }, '*');
}

3.2 Backend Error Responses

Standardize error responses:

javascript
// Error response format
{
  "error": "not_found",
  "message": "Restaurant not found",
  "code": 404,
  "details": {
    "slug": "invalid-restaurant"
  }
}

Step 4: Add Monitoring

4.1 Backend Monitoring

Use PM2 for logs:

bash
pm2 logs restaurant-api --lines 100
pm2 monit

Add structured logging:

javascript
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

app.get('/api/restaurant/:slug', async (req, res) => {
  const startTime = Date.now();

  logger.info('Restaurant request', {
    slug: req.params.slug,
    ip: req.ip
  });

  try {
    // ... your code
    const duration = Date.now() - startTime;

    logger.info('Restaurant response', {
      slug: req.params.slug,
      duration,
      itemCount: restaurant.menu_items.length
    });

  } catch (error) {
    logger.error('Restaurant error', {
      slug: req.params.slug,
      error: error.message,
      stack: error.stack
    });
  }
});

4.2 Component Analytics

Track component usage:

javascript
// Track component load
window.parent.postMessage({
  type: 'component-event',
  component: 'restaurant-menu',
  event: 'loaded',
  data: {
    loadTime: Date.now() - startTime,
    restaurantSlug: props.restaurantSlug
  }
}, '*');

// Track user interactions
function selectItem(itemId) {
  window.parent.postMessage({
    type: 'component-event',
    component: 'restaurant-menu',
    event: 'item-selected',
    data: {
      itemId,
      timestamp: Date.now()
    }
  }, '*');
}

Step 5: Performance Optimization

5.1 Enable Caching

Update board data provider:

json
{
  "data_provider": {
    "cache": {
      "enabled": true,
      "ttl": 300,
      "vary_by": ["restaurantSlug"]
    }
  }
}

5.2 Optimize Component Size

Before upload, minify files:

bash
# Install minification tools
npm install -g html-minifier clean-css-cli uglify-js

# Minify HTML
html-minifier --collapse-whitespace --remove-comments index.html -o index.min.html

# Minify CSS (if separate)
cleancss -o styles.min.css styles.css

# Minify JS (if separate)
uglifyjs script.js -o script.min.js -c -m

5.3 Lazy Load Images

javascript
// Use loading="lazy" for images
<img src="${item.image}" loading="lazy" alt="${item.name}">

5.4 Backend Response Optimization

javascript
// Limit response size
app.get('/api/restaurant/:slug', async (req, res) => {
  const restaurant = await db.restaurants.findOne({
    where: { slug },
    attributes: ['name', 'cuisine', 'logo_url'], // Only needed fields
    include: [{
      model: 'menu_items',
      attributes: ['id', 'name', 'description', 'price', 'category'],
      limit: 50 // Prevent huge responses
    }]
  });

  // Enable gzip compression
  res.set('Content-Encoding', 'gzip');
  res.json(restaurant);
});

Step 6: Versioning Strategy

6.1 Semantic Versioning

Follow semver: MAJOR.MINOR.PATCH

  • MAJOR: Breaking changes (1.0.0 → 2.0.0)
  • MINOR: New features (1.0.0 → 1.1.0)
  • PATCH: Bug fixes (1.0.0 → 1.0.1)

6.2 Publishing Updates

bash
# Update manifest.json version
{
  "version": "1.1.0"
}

# Create new ZIP
zip -r restaurant-menu-1.1.0.zip manifest.json index.html

# Upload new version
curl -X POST https://app.boardapi.io/api/v1/components/publish \
  -F "bundle=@restaurant-menu-1.1.0.zip" \
  -F "apiKey=YOUR_API_KEY"

6.3 Update Board Components

bash
# Update board to use new version
curl -X PATCH https://app.boardapi.io/api/v1/boards/{BOARD_UUID} \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "components": [
        {
          "type": "restaurant-menu",
          "url": "https://app.boardapi.io/components/restaurant-menu@1.1.0/index.html",
          "version": "1.1.0"
        }
      ]
    }
  }'

Step 7: Security Best Practices

7.1 Component Security

javascript
// Validate all external data
function sanitizeData(data) {
  if (!data || typeof data !== 'object') {
    throw new Error('Invalid data format');
  }

  if (!data.name || !data.menu || !Array.isArray(data.menu)) {
    throw new Error('Missing required fields');
  }

  return {
    name: String(data.name).slice(0, 100),
    cuisine: String(data.cuisine || '').slice(0, 50),
    menu: data.menu.slice(0, 100).map(item => ({
      id: parseInt(item.id),
      name: String(item.name).slice(0, 200),
      description: String(item.description || '').slice(0, 500),
      price: parseFloat(item.price),
      category: String(item.category).slice(0, 50)
    }))
  };
}

7.2 Backend Security

javascript
// Input validation
const { body, validationResult } = require('express-validator');

app.get('/api/restaurant/:slug',
  // Validate slug format
  param('slug').isSlug().isLength({ max: 50 }),
  authenticateApiKey,
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    // ... your code
  }
);

7.3 CORS Configuration

javascript
const cors = require('cors');

app.use(cors({
  origin: [
    'https://app.boardapi.io',
    'https://boardapi.io'
  ],
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Production Checklist

Before going live:

Component

  • [ ] Version number updated
  • [ ] Error handling implemented
  • [ ] Loading states added
  • [ ] File size < 500KB
  • [ ] No console.log() in production
  • [ ] Analytics events added

Backend

  • [ ] Authentication enabled
  • [ ] Rate limiting configured
  • [ ] Error logging setup
  • [ ] Response time < 3s
  • [ ] CORS configured
  • [ ] HTTPS enabled

Data Provider

  • [ ] Webhook URL is production
  • [ ] API keys secured
  • [ ] Cache configured
  • [ ] Retry logic enabled
  • [ ] Timeout set (5s)

Testing

  • [ ] Component loads in < 3s
  • [ ] Error states tested
  • [ ] Multi-user sync works
  • [ ] Mobile responsive
  • [ ] Cross-browser tested

Monitoring Dashboard

Track key metrics:

javascript
// Example monitoring script
const metrics = {
  componentLoads: 0,
  apiCalls: 0,
  errors: 0,
  avgLoadTime: 0
};

window.addEventListener('message', (event) => {
  if (event.data.type === 'component-event') {
    const { event: eventName, data } = event.data;

    switch(eventName) {
      case 'loaded':
        metrics.componentLoads++;
        metrics.avgLoadTime =
          (metrics.avgLoadTime + data.loadTime) / 2;
        break;

      case 'error':
        metrics.errors++;
        // Send to monitoring service
        sendToMonitoring('error', data);
        break;
    }

    // Update dashboard
    updateDashboard(metrics);
  }
});

Troubleshooting Guide

Webhook not called

bash
# Test webhook directly
curl -v https://api.yourcompany.com/api/restaurant/test \
  -H "Authorization: Bearer YOUR_KEY"

# Check BoardAPI logs
# (Contact support for access)

Slow response times

  • Check database query performance
  • Enable caching (ttl: 300)
  • Optimize database indexes
  • Use CDN for static assets

Cache not working

  • Verify cache.enabled: true
  • Check vary_by matches params
  • Confirm ttl is set
  • Test with different restaurantSlug values

Next Steps

Support


🎉 Congratulations! You've deployed a production-ready component with external data integration, monitoring, and best practices.