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 --> AStep 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 mainOption 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 startupOption 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.json2.2 Data Provider Configuration Explained
| Field | Description | Default |
|---|---|---|
webhook_url | Your backend API endpoint | Required |
method | HTTP method (GET/POST) | GET |
headers | Authentication headers | {} |
timeout | Max response time (ms) | 5000 |
retry.max_attempts | Retry failed requests | 3 |
retry.backoff | Retry strategy | exponential |
cache.enabled | Cache responses | false |
cache.ttl | Cache 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 monitAdd 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 -m5.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_bymatches params - Confirm
ttlis set - Test with different
restaurantSlugvalues
Next Steps
- API Reference - Full API documentation
- Webhook Reference - Webhook specs
- Examples - More component examples
Support
- Documentation: docs.boardapi.io
- Status Page: status.boardapi.io
- Support: support@boardapi.io
🎉 Congratulations! You've deployed a production-ready component with external data integration, monitoring, and best practices.