Security Best Practices
API Key Management
Critical: Never expose your API key in client-side code. Always call the Doshi API from your backend.
Copy
// ✅ Correct - Backend handles API key
// backend/api/auth.ts
app.post('/generate-doshi-token', async (req, res) => {
const response = await fetch('https://api.doshi.app/client/auth/token', {
headers: {
'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`
}
});
// ...
});
// ❌ Never do this - API key exposed
// frontend/app.tsx
const response = await fetch('https://api.doshi.app/client/auth/token', {
headers: {
'Authorization': 'Bearer sk_live_abc123...' // EXPOSED!
}
});
Environment Variables
Store your API key securely:Copy
# .env
DOSHI_API_KEY=your_api_key_here
DOSHI_API_URL=https://api.doshi.app
Copy
// config.ts
export const config = {
doshiApiKey: process.env.DOSHI_API_KEY,
doshiApiUrl: process.env.DOSHI_API_URL,
};
// Never commit .env files
// Add to .gitignore:
// .env
// .env.local
Always Use HTTPS
Copy
// ✅ Correct
const webviewUrl = "https://embed.v2.doshi.app";
const apiUrl = "https://api.doshi.app";
// ❌ Never use HTTP in production
const insecureUrl = "http://embed.v2.doshi.app";
Implement Proper Origin Checks
Copy
// For postMessage
window.addEventListener("message", (event) => {
// ✅ Always verify origin in production
if (event.origin !== 'https://embed.v2.doshi.app') {
console.warn('Rejected message from:', event.origin);
return;
}
// Process message
});
Never Use Wildcards in Production
Copy
// ❌ Development/testing only
webview.contentWindow.postMessage(data, "*");
// ✅ Production - always specify exact origin
webview.contentWindow.postMessage(data, "https://embed.v2.doshi.app");
Validate All Data
Copy
function validateAuthData(data: any): data is AuthData {
if (!data || typeof data !== 'object') {
throw new Error('Invalid data format');
}
if (!data.token || typeof data.token !== 'string') {
throw new Error('Missing or invalid token');
}
if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
throw new Error('Invalid email format');
}
return true;
}
// Use in your code
try {
validateAuthData(authData);
// Proceed with authentication
} catch (error) {
console.error('Validation failed:', error);
// Handle error
}
Performance Best Practices
Minimize API Calls
Cache the nonce token temporarily if you need to render multiple iframes for the same user session
Copy
// Token cache with expiration
class TokenCache {
private cache = new Map<string, { token: string; expires: number }>();
private readonly TTL = 5 * 60 * 1000; // 5 minutes
async getToken(userId: string): Promise<string> {
const cached = this.cache.get(userId);
if (cached && cached.expires > Date.now()) {
return cached.token;
}
// Fetch new token
const token = await this.fetchNewToken(userId);
this.cache.set(userId, {
token,
expires: Date.now() + this.TTL
});
return token;
}
private async fetchNewToken(userId: string): Promise<string> {
// Call your backend
const response = await fetch('/api/generate-doshi-token', {
method: 'POST',
body: JSON.stringify({ userId })
});
const data = await response.json();
return data.token;
}
}
const tokenCache = new TokenCache();
Optimize Message Size
Copy
// ✅ Send all data in a single message
const authData = {
token: user.token,
email: user.email,
segment: user.segment,
branchId: user.branchId,
type: "AUTH"
};
webview.contentWindow.postMessage(JSON.stringify(authData), origin);
// ❌ Avoid multiple sequential messages
webview.contentWindow.postMessage(JSON.stringify({ token }), origin);
webview.contentWindow.postMessage(JSON.stringify({ email }), origin);
webview.contentWindow.postMessage(JSON.stringify({ segment }), origin);
Implement Proper Cleanup
Copy
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Handle message
};
window.addEventListener("message", handleMessage);
// ✅ Always cleanup
return () => {
window.removeEventListener("message", handleMessage);
};
}, []);
Lazy Load the Iframe
Copy
import React, { useState, useEffect } from 'react';
const LazyWebview: React.FC = () => {
const [shouldLoad, setShouldLoad] = useState(false);
useEffect(() => {
// Load iframe only when needed
const timer = setTimeout(() => {
setShouldLoad(true);
}, 1000);
return () => clearTimeout(timer);
}, []);
if (!shouldLoad) {
return <div>Preparing...</div>;
}
return <WebviewComponent />;
};
Preconnect to Doshi Domain
Copy
<!-- Add to <head> for faster iframe loading -->
<link rel="preconnect" href="https://embed.doshi.app">
<link rel="dns-prefetch" href="https://embed.doshi.app">
Mobile App Integration
When embedding Doshi Frontend in a mobile app (iOS/Android WebView), there are special considerations:Disable Zoom
Zooming should be disabled on mobile to prevent layout issues and ensure consistent user experience.
Copy
<!-- Add to your <head> tag -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
Copy
// Add to your main HTML file or use react-helmet
import { Helmet } from 'react-helmet';
function App() {
return (
<>
<Helmet>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</Helmet>
<DoshiEmbed />
</>
);
}
Copy
// Disable pinch to zoom
webView.scrollView.isScrollEnabled = false
webView.scrollView.bounces = false
// For WKWebView
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.scrollView.isScrollEnabled = false
// Inject viewport meta tag
let viewportScript = """
var meta = document.createElement('meta');
meta.setAttribute('name', 'viewport');
meta.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
document.getElementsByTagName('head')[0].appendChild(meta);
"""
let userScript = WKUserScript(source: viewportScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)
Copy
// In your Activity or Fragment
webView.settings.apply {
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
// Load HTML with viewport meta tag
val htmlContent = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<iframe src="https://embed.doshi.app?token=YOUR_TOKEN"
style="width:100%; height:100vh; border:none;">
</iframe>
</body>
</html>
"""
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
Handle Keyboard in Mobile Apps
When the keyboard opens in mobile apps, it can squeeze the iframe height and make content not visible. Handle this in your app environment.
Copy
class ViewController: UIViewController {
var webView: WKWebView!
var webViewBottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Setup WebView with constraints
webView = WKWebView()
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
webViewBottomConstraint = webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
webViewBottomConstraint.isActive = true
// Observe keyboard notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
@objc func keyboardWillShow(_ notification: Notification) {
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
return
}
// Adjust WebView bottom constraint
webViewBottomConstraint.constant = -keyboardFrame.height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
@objc func keyboardWillHide(_ notification: Notification) {
// Reset WebView bottom constraint
webViewBottomConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Copy
class WebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set window to adjust resize when keyboard appears
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContentView(R.layout.activity_webview)
webView = findViewById(R.id.webView)
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
// Optional: Listen to keyboard visibility
val rootView = findViewById<View>(android.R.id.content)
rootView.viewTreeObserver.addOnGlobalLayoutListener {
val heightDiff = rootView.rootView.height - rootView.height
if (heightDiff > 200) {
// Keyboard is visible
onKeyboardVisible(heightDiff)
} else {
// Keyboard is hidden
onKeyboardHidden()
}
}
}
private fun onKeyboardVisible(keyboardHeight: Int) {
// Adjust WebView or scroll to focused input
webView.evaluateJavascript("""
(function() {
var activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
})();
""", null)
}
private fun onKeyboardHidden() {
// Reset any adjustments if needed
}
}
Copy
import { KeyboardAvoidingView, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
function DoshiWebView() {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<WebView
source={{ uri: 'https://embed.doshi.app?token=YOUR_TOKEN' }}
style={{ flex: 1 }}
scrollEnabled={false}
scalesPageToFit={false}
/>
</KeyboardAvoidingView>
);
}
Copy
<!-- In AndroidManifest.xml -->
<activity
android:name=".WebViewActivity"
android:windowSoftInputMode="adjustResize">
</activity>
Best Practices for Mobile
Always disable zoom for consistent experience
Handle keyboard visibility in your app wrapper
Use
adjustResize on Android to resize layout when keyboard appearsUse constraint-based layout on iOS to adjust for keyboard
Test on various screen sizes and keyboard types
Ensure input fields remain visible when keyboard is open
Handling Link Clicks
Doshi Frontend cannot handle external link clicks that open in new windows/popups due to security restrictions.
Why Popups Are Blocked
For security reasons, Doshi Frontend does not support opening links in popups or new windows from within the iframe. This prevents:- Phishing attacks
- Unauthorized redirects
- Cross-site scripting vulnerabilities
postMessage Callback for Link Clicks
When users click on external links within Doshi Frontend, the iframe will send a postMessage to your parent application. You must handle these messages and open links in your environment. Listen for Link Click Messages:Copy
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin !== 'https://embed.doshi.app') return;
try {
const data = typeof event.data === 'string'
? JSON.parse(event.data)
: event.data;
// Handle external link clicks
if (data.type === 'EXTERNAL_LINK') {
const { url, title } = data;
// Open in same tab
window.location.href = url;
// Or open in new tab
// window.open(url, '_blank');
// Or handle in your app's navigation
// navigateToExternal(url);
}
} catch (error) {
console.error('Error handling message:', error);
}
});
Copy
// Add WKScriptMessageHandler
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if message.name == "linkHandler" {
if let body = message.body as? [String: Any],
let url = body["url"] as? String {
// Handle the link
if let linkUrl = URL(string: url) {
// Option 1: Open in Safari
UIApplication.shared.open(linkUrl)
// Option 2: Open in SFSafariViewController
let safariVC = SFSafariViewController(url: linkUrl)
present(safariVC, animated: true)
}
}
}
}
}
// Setup message handler
override func viewDidLoad() {
super.viewDidLoad()
let contentController = webView.configuration.userContentController
contentController.add(self, name: "linkHandler")
// Inject message handler script
let script = """
window.addEventListener('message', function(event) {
if (event.data.type === 'EXTERNAL_LINK') {
window.webkit.messageHandlers.linkHandler.postMessage({
url: event.data.url,
title: event.data.title
});
}
});
"""
let userScript = WKUserScript(
source: script,
injectionTime: .atDocumentEnd,
forMainFrameOnly: false
)
contentController.addUserScript(userScript)
}
Copy
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val url = request?.url.toString()
// If it's an external link, handle it in your app
if (url.startsWith("http") && !url.contains("embed.doshi.app")) {
// Option 1: Open in external browser
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
return true
// Option 2: Open in Chrome Custom Tabs
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(this@WebViewActivity, Uri.parse(url))
return true
}
return false
}
}
// Add JavaScript interface for postMessage
webView.addJavascriptInterface(object {
@JavascriptInterface
fun handleExternalLink(url: String, title: String) {
runOnUiThread {
// Handle the link
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
}
}, "AndroidInterface")
// Inject message listener
webView.evaluateJavascript("""
window.addEventListener('message', function(event) {
if (event.data.type === 'EXTERNAL_LINK') {
AndroidInterface.handleExternalLink(event.data.url, event.data.title);
}
});
""", null)
Copy
import React, { useEffect } from 'react';
function DoshiEmbed() {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== 'https://embed.doshi.app') return;
try {
const data = typeof event.data === 'string'
? JSON.parse(event.data)
: event.data;
if (data.type === 'EXTERNAL_LINK') {
// Option 1: Navigate in same window
window.location.href = data.url;
// Option 2: Open in new tab
// window.open(data.url, '_blank', 'noopener,noreferrer');
// Option 3: Use React Router (if applicable)
// navigate(`/external?url=${encodeURIComponent(data.url)}`);
// Option 4: Show confirmation dialog
// if (confirm(`Open ${data.title || 'link'}?`)) {
// window.open(data.url, '_blank');
// }
}
} catch (error) {
console.error('Error handling link message:', error);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
return (
<iframe
src="https://embed.doshi.app"
className="w-full h-screen"
frameBorder="0"
allowFullScreen
/>
);
}
Message Format
When a user clicks an external link, Doshi Frontend sends:Copy
{
type: 'EXTERNAL_LINK',
url: 'https://example.com/article',
title: 'Article Title', // Optional
timestamp: 1234567890
}
Security Considerations
Always verify
event.origin before processing messagesValidate URLs before opening (check for malicious protocols)
Use
noopener,noreferrer when opening links in new tabsConsider showing user confirmation for external links
Log external link clicks for security auditing
URL Validation Example
Copy
function isValidUrl(url: string): boolean {
try {
const urlObj = new URL(url);
// Only allow http and https protocols
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false;
}
// Optional: Whitelist or blacklist domains
const blockedDomains = ['malicious-site.com', 'phishing-site.com'];
if (blockedDomains.some(domain => urlObj.hostname.includes(domain))) {
return false;
}
return true;
} catch {
return false;
}
}
// Use in message handler
if (data.type === 'EXTERNAL_LINK') {
if (isValidUrl(data.url)) {
window.open(data.url, '_blank', 'noopener,noreferrer');
} else {
console.warn('Invalid or blocked URL:', data.url);
}
}
Error Handling Best Practices
Comprehensive Error Handling
Copy
const handleAuthentication = async () => {
try {
// Attempt to get token
const token = await fetchToken();
if (!token) {
throw new Error('No token received');
}
// Attempt to send to iframe
await sendAuthToIframe(token);
} catch (error) {
// Log error
console.error('Authentication error:', error);
// Send to monitoring service
if (window.analytics) {
window.analytics.track('Authentication Failed', {
error: error.message,
timestamp: new Date().toISOString()
});
}
// Show user-friendly message
setError('Unable to authenticate. Please try again.');
// Optionally retry
if (retryCount < MAX_RETRIES) {
setTimeout(() => {
setRetryCount(retryCount + 1);
handleAuthentication();
}, RETRY_DELAY);
}
}
};
User Feedback
Copy
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Show appropriate UI based on status
return (
<div>
{status === 'loading' && (
<div className="loading-state">
<Spinner />
<p>Authenticating...</p>
</div>
)}
{status === 'error' && (
<div className="error-state">
<ErrorIcon />
<p>{errorMessage}</p>
<button onClick={retry}>Retry</button>
</div>
)}
{status === 'success' && (
<iframe src={webviewUrl} />
)}
</div>
);
Timeout Handling
Copy
const AUTH_TIMEOUT = 10000; // 10 seconds
const authenticateWithTimeout = () => {
return Promise.race([
authenticate(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Authentication timeout')), AUTH_TIMEOUT)
)
]);
};
try {
await authenticateWithTimeout();
} catch (error) {
if (error.message === 'Authentication timeout') {
setError('Authentication is taking longer than expected. Please check your connection.');
}
}
Method Selection Guide
When to Use postMessage
Handling sensitive authentication data
Need for real-time, bidirectional communication
Production environments
Multiple authentication steps
Maximum security requirements
Complex 2FA flows
Copy
// Use postMessage for production
const ProductionWebview = ({ userData }) => {
// postMessage implementation
// More secure, no data in URLs
};
When to Use Query Parameters
Simple, one-time authentication
Rapid prototyping and development
Need for easier debugging
Simpler implementation requirements
Quick demos or proof of concepts
Copy
// Use query params for development/testing
const DevelopmentWebview = ({ userData }) => {
const url = `https://embed.doshi.app?token=${token}`;
return <iframe src={url} />;
};
Code Organization
Separation of Concerns
Copy
// services/doshi-auth.service.ts
export class DoshiAuthService {
private apiUrl: string;
private embedUrl: string;
constructor(config: Config) {
this.apiUrl = config.apiUrl;
this.embedUrl = config.embedUrl;
}
async getAuthToken(userData: UserData): Promise<string> {
const response = await fetch(`${this.apiUrl}/generate-token`, {
method: 'POST',
body: JSON.stringify(userData)
});
const data = await response.json();
return data.token;
}
buildEmbedUrl(token: string, params: AuthParams): string {
const urlParams = new URLSearchParams({ token, ...params });
return `${this.embedUrl}?${urlParams.toString()}`;
}
}
// components/DoshiEmbed.tsx
export const DoshiEmbed: React.FC<Props> = ({ userData }) => {
const authService = useDoshiAuthService();
// Component logic
};
Configuration Management
Copy
// config/doshi.config.ts
export const doshiConfig = {
development: {
apiUrl: 'https://sandbox.api.doshi.app',
embedUrl: 'https://staging-embed.doshi.app',
enableDebugLogs: true,
strictOriginCheck: false
},
production: {
apiUrl: 'https://production-doshi-api-8kq2.encr.app',
embedUrl: 'https://embed.doshi.app',
enableDebugLogs: false,
strictOriginCheck: true
}
};
export const getConfig = () => {
const env = process.env.NODE_ENV || 'development';
return doshiConfig[env];
};
Styling Best Practices
Responsive Container
Copy
.webview-container {
/* Full viewport height */
width: 100%;
height: 100vh;
/* Prevent overflow */
overflow: hidden;
/* Optional: Add subtle border */
border: 1px solid #e5e7eb;
border-radius: 8px;
}
/* Mobile responsive */
@media (max-width: 768px) {
.webview-container {
/* Adjust for mobile viewport */
height: calc(100vh - 60px); /* Account for mobile browser UI */
}
}
/* Tablet */
@media (min-width: 769px) and (max-width: 1024px) {
.webview-container {
height: calc(100vh - 80px);
}
}
Loading States
Copy
const LoadingOverlay: React.FC = () => (
<div className="absolute inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-solid border-blue-600 border-r-transparent"></div>
<p className="mt-4 text-gray-700">Loading Doshi...</p>
</div>
</div>
);
// Usage
<div className="relative webview-container">
{isLoading && <LoadingOverlay />}
<iframe src={url} onLoad={() => setIsLoading(false)} />
</div>
Full-Screen Support
Copy
.webview-container iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
/* Allow fullscreen */
.webview-container iframe:fullscreen {
width: 100vw;
height: 100vh;
}
Testing Best Practices
Environment-Specific Testing
Copy
describe('Doshi Authentication', () => {
beforeEach(() => {
// Use sandbox environment for testing
process.env.DOSHI_API_URL = 'https://sandbox.api.doshi.app';
});
it('should generate auth token', async () => {
const token = await generateAuthToken({
email: '[email protected]'
});
expect(token).toBeDefined();
expect(typeof token).toBe('string');
});
it('should handle authentication errors', async () => {
// Test error handling
await expect(
generateAuthToken({ email: 'invalid' })
).rejects.toThrow();
});
});
Mock API Responses
Copy
// __mocks__/doshi-api.ts
export const mockDoshiApi = {
generateToken: jest.fn().mockResolvedValue({
token: 'mock_token_123',
userId: 'user_123',
isNewUser: false
}),
mockError: jest.fn().mockRejectedValue(
new Error('API Error')
)
};
Integration Testing
Copy
import { render, screen, waitFor } from '@testing-library/react';
test('displays webview after successful authentication', async () => {
render(<DoshiEmbed userEmail="[email protected]" />);
// Should show loading initially
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Should show iframe after auth
await waitFor(() => {
expect(screen.getByTitle('Doshi Embed')).toBeInTheDocument();
});
});
Monitoring and Logging
Track Key Events
Copy
// Track authentication flow
const trackAuthEvent = (event: string, metadata?: any) => {
if (window.analytics) {
window.analytics.track(event, {
...metadata,
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV
});
}
};
// Usage
trackAuthEvent('Auth Token Requested', { userId: user.id });
trackAuthEvent('Auth Token Received', { userId: user.id });
trackAuthEvent('Iframe Loaded', { userId: user.id });
Error Monitoring
Copy
// Send errors to monitoring service
const logError = (error: Error, context?: any) => {
console.error('Doshi Auth Error:', error);
// Send to error tracking service (e.g., Sentry)
if (window.Sentry) {
window.Sentry.captureException(error, {
extra: context
});
}
};
// Usage
try {
await authenticate();
} catch (error) {
logError(error, {
userId: user.id,
action: 'authentication'
});
}
Performance Monitoring
Copy
// Measure authentication performance
const measureAuthPerformance = async () => {
const startTime = performance.now();
try {
await authenticate();
const endTime = performance.now();
const duration = endTime - startTime;
// Log performance metric
console.log(`Authentication took ${duration}ms`);
// Send to analytics
if (window.analytics) {
window.analytics.track('Auth Performance', {
duration,
status: 'success'
});
}
} catch (error) {
const endTime = performance.now();
const duration = endTime - startTime;
if (window.analytics) {
window.analytics.track('Auth Performance', {
duration,
status: 'error'
});
}
}
};
Production Checklist
API key stored securely in environment variables
API calls made from backend only
HTTPS used for all URLs
Origin verification enabled for postMessage
No ”*” wildcards in production
Comprehensive error handling implemented
User feedback for all states (loading, error, success)
Timeout handling for slow connections
Proper cleanup of event listeners
Error logging and monitoring configured
Performance tracking in place
Tested across multiple browsers
Tested on mobile devices
Responsive design implemented
Accessibility considerations addressed
