Skip to content
On this page

Security Best Practices ​

Follow these best practices to ensure your Flowless integration is secure and production-ready.


Bridge Secret Management ​

Never Expose Your Bridge Secret ​

DANGER

Critical Security Rule Never commit your Bridge Secret to version control or expose it in client-side code!

✅ DO ​

bash
# .env file (add to .gitignore)
BRIDGE_SECRET=bridge_secret_abc123xyz789
typescript
// Backend only
const bridgeSecret = process.env.BRIDGE_SECRET;

❌ DON'T ​

typescript
// NEVER do this!
const bridgeSecret = 'bridge_secret_abc123xyz789'; // Hardcoded
javascript
// NEVER in frontend!
const response = await fetch('/bridge/validate', {
  headers: { 'X-Bridge-Secret': 'bridge_secret_abc123xyz789' }
});

Rotate Secrets Regularly ​

  • Frequency: Every 90 days minimum
  • After breach: Immediately
  • When employee leaves: Within 24 hours

Session Security ​

Use HTTPS Only ​

WARNING

Always use HTTPS in production. Never send session IDs over HTTP.

typescript
// ✅ Good
const FLOWLESS_URL = 'https://your-instance.pubflow.com';

// ❌ Bad
const FLOWLESS_URL = 'http://your-instance.pubflow.com';

Store Sessions Securely ​

Frontend Storage ​

typescript
// ✅ Best: HttpOnly cookies (if possible)
// Set by your backend
res.cookie('session_id', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// ✅ Good: localStorage (for SPAs)
localStorage.setItem('session_id', sessionId);

// ❌ Avoid: sessionStorage (lost on tab close)
sessionStorage.setItem('session_id', sessionId);

Mobile Apps ​

typescript
// React Native - Use AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.setItem('session_id', sessionId);

Clear Sessions on Logout ​

typescript
async function logout() {
  const sessionId = localStorage.getItem('session_id');
  
  // 1. Call Flowless logout
  await fetch(`${FLOWLESS_URL}/auth/logout`, {
    method: 'POST',
    headers: { 'X-Session-ID': sessionId },
  });
  
  // 2. Clear local storage
  localStorage.removeItem('session_id');
  localStorage.removeItem('user_data');
  
  // 3. Redirect to login
  window.location.href = '/login';
}

Validation Mode Selection ​

Choose the Right Mode ​

typescript
// Low-security apps (blogs, public content)
VALIDATION_MODE=STANDARD

// Medium-security apps (e-commerce, SaaS)
VALIDATION_MODE=ADVANCED

// High-security apps (banking, healthcare)
VALIDATION_MODE=STRICT

STANDARD Mode ​

  • ✅ Fast validation
  • ✅ Works with dynamic IPs
  • ⚠️ Less secure against session hijacking

ADVANCED Mode ​

  • ✅ Device binding
  • ✅ Better mobile security
  • ⚠️ Requires device ID management

STRICT Mode ​

  • ✅ Maximum security
  • ✅ Detects browser changes
  • ⚠️ May break with browser updates

Password Security ​

Enforce Strong Passwords ​

json
{
  "password_min_length": 12,
  "password_require_uppercase": true,
  "password_require_lowercase": true,
  "password_require_numbers": true,
  "password_require_special": true
}

Validate on Frontend ​

typescript
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  
  if (password.length < 12) {
    errors.push('Password must be at least 12 characters');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain uppercase letter');
  }
  if (!/[a-z]/.test(password)) {
    errors.push('Password must contain lowercase letter');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Password must contain number');
  }
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('Password must contain special character');
  }
  
  return errors;
}

Never Log Passwords ​

typescript
// ❌ NEVER do this!
console.log('User password:', password);
logger.info(`Login attempt with password: ${password}`);

// ✅ Log safely
logger.info(`Login attempt for user: ${email}`);

Rate Limiting ​

Respect Rate Limits ​

typescript
async function loginWithRetry(email: string, password: string) {
  const response = await fetch(`${FLOWLESS_URL}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  
  if (response.status === 429) {
    const resetTime = response.headers.get('X-RateLimit-Reset');
    const waitSeconds = parseInt(resetTime!) - Math.floor(Date.now() / 1000);
    
    throw new Error(`Rate limited. Try again in ${waitSeconds} seconds`);
  }
  
  return response.json();
}

Implement Client-Side Rate Limiting ​

typescript
class RateLimiter {
  private attempts: number[] = [];
  
  canAttempt(maxAttempts: number, windowMs: number): boolean {
    const now = Date.now();
    this.attempts = this.attempts.filter(time => now - time < windowMs);
    
    if (this.attempts.length >= maxAttempts) {
      return false;
    }
    
    this.attempts.push(now);
    return true;
  }
}

const loginLimiter = new RateLimiter();

async function login(email: string, password: string) {
  if (!loginLimiter.canAttempt(5, 15 * 60 * 1000)) {
    throw new Error('Too many login attempts. Please wait 15 minutes.');
  }
  
  // Proceed with login
}

Input Validation ​

Validate Email Addresses ​

typescript
function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Always normalize to lowercase
const email = userInput.toLowerCase().trim();

Sanitize User Input ​

typescript
function sanitizeInput(input: string): string {
  return input
    .trim()
    .replace(/[<>]/g, '') // Remove HTML tags
    .substring(0, 255); // Limit length
}

Trust Token Caching ​

Cache Securely ​

typescript
import { LRUCache } from 'lru-cache';

const trustTokenCache = new LRUCache<string, string>({
  max: 10000,
  ttl: 1000 * 60 * 5, // 5 minutes max
});

// ✅ Cache trust tokens
trustTokenCache.set(sessionId, trustToken);

// ✅ Validate before use
const cachedToken = trustTokenCache.get(sessionId);
if (cachedToken && isTokenValid(cachedToken)) {
  return decodeToken(cachedToken);
}

Don't Cache Too Long ​

typescript
// ❌ Too long - security risk
ttl: 1000 * 60 * 60 // 1 hour

// ✅ Recommended
ttl: 1000 * 60 * 5 // 5 minutes

Error Handling ​

Don't Leak Information ​

typescript
// ❌ Bad - reveals if email exists
if (!user) {
  return { error: 'Email not found' };
}
if (!passwordMatch) {
  return { error: 'Incorrect password' };
}

// ✅ Good - generic message
if (!user || !passwordMatch) {
  return { error: 'Invalid email or password' };
}

Log Security Events ​

typescript
// Log failed login attempts
logger.warn('Failed login attempt', {
  email,
  ip: req.ip,
  timestamp: new Date(),
});

// Log successful logins
logger.info('Successful login', {
  userId: user.id,
  ip: req.ip,
  timestamp: new Date(),
});

Production Checklist ​

Before Going Live ​

  • [ ] Bridge Secret stored in environment variables
  • [ ] HTTPS enabled on all endpoints
  • [ ] Rate limiting configured
  • [ ] Password requirements enforced
  • [ ] Email verification enabled
  • [ ] Session duration appropriate
  • [ ] Validation mode selected
  • [ ] Error messages don't leak info
  • [ ] Logging configured
  • [ ] Monitoring enabled
  • [ ] Backup plan in place
  • [ ] Security audit completed

Incident Response ​

If Bridge Secret is Compromised ​

  1. Immediately rotate the secret in Pubflow dashboard
  2. Update environment variables in all environments
  3. Restart all backend services
  4. Invalidate all active sessions
  5. Notify users to re-login
  6. Investigate how the breach occurred
  7. Document the incident

Next Steps ​