Developer-Friendly Guide to Building Backends with Pubflow
This guide explains the 7 core concepts that power Flowfull - the custom backend layer of the Pubflow architecture.
Whether you're building with Node.js, Go, Python, or Rust, these concepts will help you create production-ready backends in record time.
✅ How Bridge Validation connects your backend to Flowless ✅ How to implement layered security with Validation Modes ✅ How to use HybridCache for lightning-fast performance ✅ How to create secure Trust Tokens with PASETO ✅ How to protect routes with Auth Middleware ✅ How to support multiple databases seamlessly ✅ How to configure everything with environment variables
- Backend Developers building APIs with Pubflow
- Architects designing scalable microservices
- Full-Stack Developers integrating frontend with Flowfull
- DevOps Engineers deploying Flowfull instances
Before diving into concepts, let's understand where Flowfull fits:
┌─────────────────────────────────────────────────────────┐
│ YOUR APPLICATION │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────┐│
│ │ FLOWLESS │ ───▶ │ FLOWFULL │ ───▶ │ CLIENT ││
│ │ │ │ │ │ ││
│ │ • Auth │ │ • Your APIs │ │ • React││
│ │ • Sessions │ │ • Business │ │ • Next ││
│ │ • Users │ │ • Database │ │ • RN ││
│ └──────────────┘ └──────────────┘ └────────┘│
│ pubflow.com This Guide! Your App │
│ │
└─────────────────────────────────────────────────────────┘
Flowfull is YOUR backend - it handles your business logic while Flowless handles authentication.
🌐 Learn more about Pubflow: pubflow.com
- Bridge Validation - Connect to Flowless for authentication
- Validation Modes - Layered security for sessions
- HybridCache System - 3-tier caching for performance
- Trust Tokens (PASETO) - Secure cryptographic tokens
- Authentication Middleware - Protect your routes
- Multi-Database Support - Use any database
- Environment Configuration - Configure with ease
Bridge Validation is the magic that connects your Flowfull backend to Flowless (the core authentication server).
Think of it as a trust bridge between your custom backend and the authentication system.
Instead of building authentication from scratch (registration, login, sessions, password reset, etc.), you:
- Use Flowless (deployed on Pubflow or self-hosted) for all auth
- Validate sessions in your Flowfull backend via Bridge Validation
- Focus on your business logic instead of auth boilerplate
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────────▶│ Flowfull │────────▶│ Flowless │
│ (Frontend) │ 1️⃣ │ (Your Backend)│ 2️⃣ │ (Auth API) │
└─────────────┘ └──────────────┘ └─────────────┘
│ 3️⃣
▼
┌──────────┐
│ Cache │
│ (LRU/Redis)│
└──────────┘
Flow:
- Client sends request with
session_idto your Flowfull backend - Flowfull asks Flowless: "Is this session valid?"
- Flowless responds with user data (cached for performance)
- Your backend processes the request with authenticated user context
// Client makes request
fetch('https://your-api.com/api/items', {
headers: {
'X-Session-ID': 'abc123...' // From Flowless login
}
});
// Your Flowfull backend
app.get('/api/items', requireAuth(), async (c) => {
// Bridge Validation already happened!
const userId = c.get('user_id'); // ✅ Authenticated user
// Your business logic
const items = await db.getItemsForUser(userId);
return c.json({ items });
});Location: src/lib/auth/bridge-validator.ts
Responsibilities:
- Validate sessions with Flowless server
- Cache valid sessions (LRU cache)
- Retry failed validations
- Sync user data (optional)
Configuration:
const bridgeValidator = new BridgeValidator({
flowlessApiUrl: 'http://localhost:3000',
validationSecret: 'your-secret-key',
timeout: 5000,
retryAttempts: 3
});// 1. Extract session_id from request
const sessionId = extractSessionId(c); // Header, Cookie, or Query
// 2. Validate with cache-first
const result = await bridgeValidator.validateSession(sessionId);
// 3. Result
if (result.success) {
// Valid session - use result.session
c.set('user_id', result.session.user_id);
c.set('session', result.session);
} else {
// Invalid session - reject request
throw new HTTPException(401, { message: result.error });
}Cache-First Architecture:
- Check Cache: Look in local LRU cache
- Validate Remote: If not in cache, validate with Flowless
- Cache Result: Save successful result in cache
- TTL: 5 minutes by default (configurable)
Benefits:
- ⚡ Performance: 97% hit rate in production
- 🔄 Resilience: Works even if Flowless is slow
- 📊 Scalability: Reduces load on central server
Flowless must expose:
POST /auth/bridge/validate?session_id=xxx
Headers:
X-Bridge-Secret: shared-secret-key
Content-Type: application/json
Response:
{
"success": true,
"session": {
"user_id": "123",
"email": "user@example.com",
"name": "John",
"user_type": "customer",
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"device_id": "device-fingerprint"
}
}
Go Example:
type BridgeValidator struct {
flowlessURL string
secret string
cache *lru.Cache
timeout time.Duration
}
func (bv *BridgeValidator) ValidateSession(sessionID string) (*Session, error) {
// 1. Check cache
if cached, ok := bv.cache.Get(sessionID); ok {
return cached.(*Session), nil
}
// 2. Validate with Flowless
session, err := bv.validateWithFlowless(sessionID)
if err != nil {
return nil, err
}
// 3. Cache result
bv.cache.Add(sessionID, session)
return session, nil
}Validation Modes is a layered security system that validates session integrity based on multiple security factors.
| Mode | IP Validation | User-Agent | Device ID | Recommended Use |
|---|---|---|---|---|
| DISABLED | ❌ | ❌ | ❌ | Testing/Development |
| STANDARD | ✅ | ❌ | ❌ | Basic production |
| ADVANCED | ✅ | ✅ | ✅ | Standard production |
| STRICT | ✅ | ✅ | ✅ | Banking/Healthcare |
Environment Variables:
AUTH_VALIDATION_MODE=ADVANCED
AUTH_ENABLE_VALIDATION_MODE=true
AUTH_IP_VALIDATION=true
AUTH_USER_AGENT_VALIDATION=true
AUTH_DEVICE_VALIDATION=true
AUTH_AUTO_INVALIDATE=false
AUTH_LOG_VIOLATIONS=trueLocation: src/lib/auth/validation-mode.ts
class ValidationMode {
validateSession(context: ValidationContext): ValidationResult {
const violations: SecurityViolation[] = [];
switch (this.config.VALIDATION_MODE) {
case 'STANDARD':
// Only validate IP
if (context.currentIP !== context.sessionIP) {
violations.push({
type: 'ip_mismatch',
severity: 'medium',
message: 'IP address changed'
});
}
break;
case 'ADVANCED':
// Validate IP + User-Agent + Device
// ... additional validations
break;
case 'STRICT':
// All validations + auto-invalidate
// ... strict validations
break;
}
return {
valid: violations.length === 0,
violations,
action: this.determineAction(violations)
};
}
}Violation Types:
ip_mismatch: IP changed since session creationuser_agent_mismatch: User-Agent changeddevice_mismatch: Device fingerprint changedsuspicious_activity: Suspicious patterns detected
Actions:
allow: Allow accesswarn: Allow but logdeny: Reject accessinvalidate: Automatically invalidate session
HybridCache is a 3-tier cache system with automatic fallback that combines Redis (distributed) + LRU (local memory) + Database (source of truth).
┌─────────────────────────────────────────┐
│ HybridCache Request │
└─────────────────────────────────────────┘
│
▼
┌────────────────┐
│ 1. Redis │ ◄── Distributed Cache
│ (Primary) │ (Multi-region)
└────────────────┘
│ MISS
▼
┌────────────────┐
│ 2. LRU Cache │ ◄── Local Memory
│ (Fallback) │ (Single instance)
└────────────────┘
│ MISS
▼
┌────────────────┐
│ 3. Database │ ◄── Source of Truth
│ (Final) │ (Persistent)
└────────────────┘
If Redis is unavailable, automatically uses local LRU cache:
async get(key: string): Promise<T | null> {
// TIER 1: Try Redis
if (this.redisAvailable && this.redis) {
const redisData = await this.redis.get(key);
if (redisData) {
// Backfill LRU cache
this.lruCache.set(key, JSON.parse(redisData));
return JSON.parse(redisData);
}
}
// TIER 2: Try LRU
const lruData = this.lruCache.get(key);
if (lruData) {
// Backfill Redis if available
if (this.redisAvailable) {
await this.redis.setex(key, this.ttl, JSON.stringify(lruData));
}
return lruData;
}
// TIER 3: Cache MISS - caller queries database
return null;
}Redis → LRU: When found in Redis, copy to LRU LRU → Redis: When found in LRU, copy to Redis
Benefit: Maximizes hit rate on both levels
interface CacheMetrics {
totalRequests: number;
cacheHits: number;
cacheMisses: number;
redisHits: number;
lruHits: number;
dbHits: number;
errors: number;
hitRate: number; // Calculated: (hits / total) * 100
}Environment Variables:
CACHE_ENABLED=true
REDIS_URL=redis://localhost:6379
CACHE_TTL=300Code:
const userCache = new HybridCache<UserData>({
cacheType: 'userContext',
ttl: 300, // 5 minutes
maxSize: 10000, // LRU max entries
keyPrefix: 'user_ctx'
});Location: src/lib/cache/cache-instances.ts (in pubflow-flowfull)
Flowfull does NOT have HybridCache implemented yet, but the concept is ready to implement:
// User Context Cache
export const userContextCache = new HybridCache<UserContext>({
cacheType: 'userContext',
ttl: 300, // 5 minutes
maxSize: 10000
});
// Permissions Cache
export const permissionsCache = new HybridCache<Permissions>({
cacheType: 'permissions',
ttl: 600, // 10 minutes
maxSize: 5000
});Go Example:
type HybridCache struct {
redis *redis.Client
lru *lru.Cache
ttl time.Duration
metrics *CacheMetrics
}
func (hc *HybridCache) Get(key string) (interface{}, error) {
// Try Redis first
if hc.redis != nil {
val, err := hc.redis.Get(ctx, key).Result()
if err == nil {
hc.lru.Add(key, val) // Backfill LRU
return val, nil
}
}
// Fallback to LRU
if val, ok := hc.lru.Get(key); ok {
return val, nil
}
return nil, ErrCacheMiss
}Trust Tokens are cryptographically secure tokens based on PASETO v4 (Platform-Agnostic Security Tokens) that replace JWT with better security and simplicity.
| Feature | JWT | PASETO |
|---|---|---|
| Algorithm | Multiple (HS256, RS256, etc.) | Ed25519 (single) |
| Security | Vulnerable to algorithm confusion | Immune to algorithm confusion |
| Simplicity | Complex (many options) | Simple (one correct way) |
| Performance | Variable | Fast and consistent |
┌──────────────────────────────────────────┐
│ PASETO Token Generation │
└──────────────────────────────────────────┘
│
▼
┌────────────────┐
│ Master Key │ ◄── Key derivation
│ (Ed25519) │
└────────────────┘
│
▼
┌────────────────┐
│ Sign Payload │ ◄── v4.public.xxxxx
│ (PASETO v4) │
└────────────────┘
│
▼
┌────────────────┐
│ Store in │ ◄── Redis + LRU
│ HybridCache │
└────────────────┘
Location: src/lib/utils/paseto-invitation-token.ts (in pubflow-flowfull)
Flowfull does NOT have PASETO implemented, but the concept is documented for future implementation.
import { V4 } from 'paseto';
async function generateInvitationToken(data: TokenPayload): Promise<string> {
const now = new Date();
const exp = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days
const payload = {
...data,
exp: exp.toISOString(),
iat: now.toISOString()
};
// Sign with Ed25519 private key
const { privateKey } = getKeyPair();
const token = await V4.sign(payload, privateKey);
// Store in HybridCache
await invitationTokenCache.set(
data.memberId,
{ status: 'pending', payload },
7 * 24 * 60 * 60 // 7 days TTL
);
return token; // Format: v4.public.xxxxx
}6 Security Layers:
async function validateInvitationToken(token: string) {
// Layer 1: PASETO signature verification (Ed25519)
const { publicKey } = getKeyPair();
const payload = await V4.verify(token, publicKey);
// Layer 2: Expiration check
if (new Date(payload.exp) < new Date()) {
return { valid: false, error: 'Token expired' };
}
// Layer 3: Redis status check
const cached = await invitationTokenCache.get(payload.memberId);
if (!cached || cached.status !== 'pending') {
return { valid: false, error: 'Token already used or invalidated' };
}
// Layer 4: DB status check (done by caller)
// Layer 5: User ownership check (done by caller)
// Layer 6: Resource validation (done by caller)
return { valid: true, payload };
}async function markTokenAsUsed(memberId: string): Promise<void> {
// Update cache status
const cached = await invitationTokenCache.get(memberId);
if (cached) {
cached.status = 'used';
await invitationTokenCache.set(memberId, cached);
}
// Update database status
await db.updateTable('organization_members')
.set({ status: 'active' })
.where('id', '=', memberId)
.execute();
}- Invitation Tokens: Invite users to organizations/projects
- Email Verification: Verify emails with secure tokens
- Password Reset: Password recovery tokens
- API Access Tokens: Temporary API access tokens
Go Example:
import "github.com/o1egl/paseto"
func GenerateToken(payload map[string]interface{}) (string, error) {
v4 := paseto.NewV4()
privateKey := getPrivateKey()
token, err := v4.Sign(privateKey, payload, nil)
if err != nil {
return "", err
}
return token, nil
}
func ValidateToken(token string) (map[string]interface{}, error) {
v4 := paseto.NewV4()
publicKey := getPublicKey()
var payload map[string]interface{}
err := v4.Verify(token, publicKey, &payload, nil)
if err != nil {
return nil, err
}
return payload, nil
}Authentication Middleware is the route protection system that validates sessions and controls access based on user types.
Requires valid session - Rejects requests without authentication
export function requireAuth() {
return async (c: Context, next: Next) => {
const sessionId = extractSessionId(c);
if (!sessionId) {
throw new HTTPException(401, {
message: 'Session ID required'
});
}
const result = await bridgeValidator.validateSession(sessionId);
if (!result.success) {
throw new HTTPException(401, {
message: result.error || 'Invalid session'
});
}
// Set user context
c.set('user_id', result.session.user_id);
c.set('session', result.session);
c.set('is_guest', false);
await next();
};
}Usage:
app.get('/api/v1/protected', requireAuth(), async (c) => {
const userId = c.get('user_id');
return c.json({ message: 'Protected data', userId });
});Allows guest access - Validates session if exists, but doesn't reject
export function optionalAuth() {
return async (c: Context, next: Next) => {
const sessionId = extractSessionId(c);
if (sessionId) {
try {
const result = await bridgeValidator.validateSession(sessionId);
if (result.success) {
c.set('user_id', result.session.user_id);
c.set('session', result.session);
c.set('is_guest', false);
} else {
c.set('is_guest', true);
}
} catch (error) {
c.set('is_guest', true);
}
} else {
c.set('is_guest', true);
}
await next();
};
}Usage:
app.get('/api/v1/items', optionalAuth(), async (c) => {
const isGuest = c.get('is_guest');
if (isGuest) {
// Return public items only
return c.json({ items: publicItems });
} else {
// Return user-specific items
const userId = c.get('user_id');
return c.json({ items: getUserItems(userId) });
}
});Validates user type - Only allows certain user_types
export function requireUserType(allowedTypes: string[]) {
return async (c: Context, next: Next) => {
const session = c.get('session');
if (!session) {
throw new HTTPException(401, {
message: 'Authentication required'
});
}
if (!allowedTypes.includes(session.user_type)) {
throw new HTTPException(403, {
message: 'Insufficient permissions'
});
}
await next();
};
}Usage:
app.delete('/api/v1/admin/users/:id',
requireAuth(),
requireUserType(['admin', 'super_admin']),
async (c) => {
// Only admins can delete users
const userId = c.req.param('id');
await deleteUser(userId);
return c.json({ success: true });
}
);Multiple sources - Header, Cookie, Query Parameter
function extractSessionId(c: Context): string | null {
// 1. Try X-Session-ID header (preferred)
const headerSession = c.req.header('X-Session-ID');
if (headerSession) return headerSession;
// 2. Try session_id cookie
const cookieSession = getCookie(c, 'session_id');
if (cookieSession) return cookieSession;
// 3. Try ?session_id=xxx query parameter
const querySession = c.req.query('session_id');
if (querySession) return querySession;
return null;
}Declarative configuration of permissions per route:
const ROUTE_PERMISSIONS: Record<string, RoutePermission> = {
'/api/v1/admin/*': {
allowedUserTypes: ['admin', 'super_admin'],
requireAuth: true,
adminOnly: true
},
'/api/v1/payments': {
allowedUserTypes: ['admin', 'user', 'guest'],
requireAuth: false, // Optional auth
ownershipCheck: true
},
'/api/v1/webhooks/*': {
allowedUserTypes: ['anonymous'],
requireAuth: false
}
};Multi-Database Support allows using different databases (PostgreSQL, MySQL, LibSQL, etc.) with the same codebase using Kysely ORM.
| Database | Protocol | Use Case |
|---|---|---|
| PostgreSQL | postgresql:// |
Standard production |
| MySQL | mysql:// |
Legacy systems |
| LibSQL/Turso | libsql:// |
Serverless SQLite |
| Neon | postgresql:// |
Serverless PostgreSQL |
| PlanetScale | Custom | Serverless MySQL |
Auto-detection based on DATABASE_URL:
export function detectDatabaseType(url: string): string {
if (url.startsWith('postgresql://') || url.startsWith('postgres://')) {
return 'postgresql';
}
if (url.startsWith('mysql://')) {
return 'mysql';
}
if (url.startsWith('libsql:')) {
return 'libsql';
}
throw new Error(`Unable to detect database type from URL: ${url}`);
}Location: src/config/database.ts
import { Kysely } from 'kysely';
import { PostgresDialect } from 'kysely';
import { MysqlDialect } from 'kysely';
import { LibsqlDialect } from '@libsql/kysely-libsql';
export async function createDatabase(config: Config) {
const dbType = detectDatabaseType(config.DATABASE_URL);
switch (dbType) {
case 'postgresql':
return new Kysely({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: config.DATABASE_URL,
ssl: config.DATABASE_SSL,
min: config.DATABASE_POOL_MIN,
max: config.DATABASE_POOL_MAX
})
})
});
case 'mysql':
return new Kysely({
dialect: new MysqlDialect({
pool: createPool({
uri: config.DATABASE_URL,
connectionLimit: config.DATABASE_POOL_MAX
})
})
});
case 'libsql':
const client = createClient({
url: config.DATABASE_URL,
authToken: config.LIBSQL_AUTH_TOKEN
});
return new Kysely({
dialect: new LibsqlDialect({ client })
});
}
}Database abstraction to facilitate testing and changes:
export class UserRepository {
constructor(private db: Kysely<Database>) {}
async findById(id: string) {
return await this.db
.selectFrom('users')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
}
async create(user: NewUser) {
return await this.db
.insertInto('users')
.values(user)
.returningAll()
.executeTakeFirstOrThrow();
}
}Environment Configuration is the centralized configuration system with Zod validation that ensures all environment variables are correctly configured.
Location: src/config/environment.ts
import { z } from 'zod';
const envSchema = z.object({
// Server Configuration
PORT: z.string().default('3001').transform(Number),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
// Database Configuration
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
// Flowless Integration
FLOWLESS_API_URL: z.string().url().default('http://localhost:3000'),
BRIDGE_VALIDATION_SECRET: z.string().min(32),
// Authentication
AUTH_VALIDATION_MODE: z.enum(['DISABLED', 'STANDARD', 'ADVANCED', 'STRICT']),
// Cache
CACHE_ENABLED: z.string().default('true').transform(val => val === 'true'),
REDIS_URL: z.string().optional()
});
export const config = envSchema.parse(process.env);Fail-fast - If configuration is invalid, server won't start:
function parseEnvironment() {
try {
return envSchema.parse(process.env);
} catch (error) {
console.error('❌ Environment validation failed:');
if (error instanceof z.ZodError) {
error.errors.forEach(err => {
console.error(` - ${err.path.join('.')}: ${err.message}`);
});
}
process.exit(1);
}
}// Bridge Validation
type BridgeValidator struct {
flowlessURL string
secret string
cache *lru.Cache
}
// HybridCache
type HybridCache struct {
redis *redis.Client
lru *lru.Cache
}
// Middleware
func RequireAuth(bv *BridgeValidator) gin.HandlerFunc {
return func(c *gin.Context) {
sessionID := extractSessionID(c)
session, err := bv.ValidateSession(sessionID)
if err != nil {
c.JSON(401, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Set("user_id", session.UserID)
c.Next()
}
}# Bridge Validation
class BridgeValidator:
def __init__(self, flowless_url: str, secret: str):
self.flowless_url = flowless_url
self.secret = secret
self.cache = LRUCache(maxsize=10000)
async def validate_session(self, session_id: str) -> Session:
# Check cache
if session_id in self.cache:
return self.cache[session_id]
# Validate with Flowless
session = await self._validate_with_flowless(session_id)
self.cache[session_id] = session
return session| Concept | Implemented in Flowfull | Implemented in Pubflow-Flowfull | Priority |
|---|---|---|---|
| Bridge Validation | ✅ Complete | ✅ Complete | 🔴 Critical |
| Validation Modes | ✅ Complete | ✅ Complete | 🟡 High |
| HybridCache | ❌ Not implemented | ✅ Complete | 🟢 Medium |
| Trust Tokens (PASETO) | ❌ Not implemented | ✅ Complete | 🟢 Medium |
| Auth Middleware | ✅ Complete | ✅ Complete | 🔴 Critical |
| Multi-Database | ✅ Complete | ✅ Complete | 🔴 Critical |
| Environment Config | ✅ Complete | ✅ Complete | 🔴 Critical |
- Implement HybridCache - Migrate from simple LRU to HybridCache with Redis
- Implement PASETO Tokens - For invitation tokens and API access
- Improve Documentation - Add more examples and use cases
- Testing - Add unit tests for all concepts
- Create Starter Kits - Templates for Go, Python, Rust
- Document Patterns - Language-specific implementation guides
- Code Examples - Functional example repositories
- Benchmarks - Compare performance across languages
- Flowfull:
2/flowfull/ - Pubflow-Flowfull:
2/pubflow-flowfull/(complete implementation) - DadosBall Server:
3/dadosball-server/(usage example) - Bridge-Payments:
2/bridge-payments/(usage example)
Flowfull is a portable core framework with well-defined concepts that can be implemented in any language. The key concepts are:
- Bridge Validation - Distributed authentication with cache
- Validation Modes - Configurable layered security
- HybridCache - 3-tier cache with fallback
- Trust Tokens - Secure tokens with PASETO
- Auth Middleware - Flexible route protection
- Multi-Database - Support for multiple databases
- Environment Config - Validated configuration with Zod
These concepts form the foundation for creating scalable, secure, and maintainable backends in any technology stack.