TVL Platform - Product Versioning Strategy
Last Updated: 2025-10-25 Status: Active Owner: Engineering & Product Leadership
Table of Contents
- Overview
- Version Structure
- Feature Flags
- Database Migrations
- API Versioning
- Client Compatibility
- Deployment Strategy
- Version Lifecycle Management
- Testing Strategy
- Rollback Procedures
Overview
Versioning Philosophy
The TVL platform follows a progressive enhancement versioning strategy that enables:
- Continuous delivery without breaking existing functionality
- Gradual feature rollout to minimize risk
- Backward compatibility across version boundaries
- Zero-downtime deployments through feature flags and blue-green deployments
- Independent client and server evolution via API versioning
Version Timeline
Month  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
       |------MVP.0-----|--MVP.1--|--MVP.2--|-----V1.0--------|-V2.0-----|--V3.0----|
Key Milestones:
- MVP.0 (Months 1-3): Foundation + Hostaway one-way sync
- MVP.1 (Months 4-5): Two-way sync + booking awareness
- MVP.2 (Months 6-7): Multi-channel distribution
- V1.0 (Months 7-12): Direct booking engine
- V2.0 (Months 13-18): Events & experiences
- V3.0 (Months 19-24): Multi-vendor marketplace
Core Principles
- Additive-Only Migrations: Database changes must never break existing code
- Feature Flag Everything: All new features gated by runtime flags
- API Compatibility: Maintain at least 2 major API versions simultaneously
- Client Graceful Degradation: Older clients must continue functioning
- Zero-Downtime Deployments: Rolling deployments without service interruption
Version Structure
Version Naming Convention
[STAGE].[MAJOR].[MINOR].[PATCH]
Examples:
- MVP.0.0.0
- MVP.1.2.1
- V1.0.0.0
- V2.1.3.5
Version Components
Stage Identifier
| Stage | Purpose | Duration | Stability | 
|---|---|---|---|
| MVP.0 | Foundation & proof of concept | 3 months | Experimental | 
| MVP.1 | Core functionality validation | 2 months | Beta | 
| MVP.2 | Pre-production stabilization | 2 months | Release Candidate | 
| V1.x | Production-ready platform | 6 months | Stable | 
| V2.x | Feature expansion | 6 months | Stable | 
| V3.x | Enterprise scale | 6 months | Stable | 
Major Version (X.0.0.0)
Incremented when:
- New stage begins (MVP → V1, V1 → V2)
- Breaking API changes required
- Significant architectural changes
- New business capabilities unlocked
Examples:
- MVP.0 → MVP.1: Two-way sync capability added
- V1.0 → V2.0: Events & experiences domain introduced
Minor Version (X.Y.0.0)
Incremented when:
- New features added within a stage
- Non-breaking API enhancements
- New integrations or channels added
- Performance improvements
Examples:
- V1.0 → V1.1: Booking.com integration added
- V2.1 → V2.2: Event packages feature launched
Patch Version (X.Y.Z.0)
Incremented when:
- Bug fixes
- Security patches
- Performance optimizations
- Documentation updates
Examples:
- V1.2.3 → V1.2.4: Fixed double-booking edge case
- V2.0.1 → V2.0.2: Security patch for authentication
Feature Flags
Feature Flag Architecture
// Database-driven feature flags
interface FeatureFlag {
  id: string;
  name: string;
  description: string;
  enabled: boolean;
  min_version: string;        // Minimum app version required
  rollout_percentage: number; // 0-100 for gradual rollout
  account_allowlist: string[]; // Specific accounts with access
  created_at: timestamp;
  updated_at: timestamp;
}
Flag Types
1. Version Gates
Purpose: Enable features only for specific versions
// Example: Events feature only for V2.0+
{
  name: "events_management",
  min_version: "V2.0.0.0",
  enabled: true,
  rollout_percentage: 100
}
Usage:
if (featureFlags.isEnabled('events_management', currentVersion)) {
  // Show events UI
}
2. Rollout Gates
Purpose: Gradual feature rollout to percentage of users
// Example: New pricing engine to 25% of users
{
  name: "dynamic_pricing_v2",
  enabled: true,
  rollout_percentage: 25
}
Usage:
const userId = getCurrentUser().id;
if (featureFlags.isEnabledForUser('dynamic_pricing_v2', userId)) {
  // Use new pricing engine
}
3. Account Allowlist Gates
Purpose: Enable features for specific beta customers
// Example: Marketplace for pilot vendors only
{
  name: "marketplace_vendor_portal",
  enabled: true,
  account_allowlist: ["acct_123", "acct_456"]
}
Usage:
const accountId = getCurrentAccount().id;
if (featureFlags.isEnabledForAccount('marketplace_vendor_portal', accountId)) {
  // Show vendor portal
}
Feature Flag Database Schema
CREATE TABLE feature_flags (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE,
  description TEXT,
  enabled BOOLEAN DEFAULT false,
  min_version TEXT,
  max_version TEXT,
  rollout_percentage INTEGER DEFAULT 0 CHECK (rollout_percentage BETWEEN 0 AND 100),
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE feature_flag_account_allowlist (
  flag_id UUID REFERENCES feature_flags(id) ON DELETE CASCADE,
  account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
  PRIMARY KEY (flag_id, account_id)
);
CREATE INDEX idx_feature_flags_name ON feature_flags(name);
CREATE INDEX idx_feature_flags_enabled ON feature_flags(enabled);
Runtime Version Detection
// Client version from request headers
const clientVersion = req.headers['x-app-version'] || 'MVP.0.0.0';
// Parse version
function parseVersion(versionString: string) {
  const [stage, major, minor, patch] = versionString.split('.');
  return { stage, major: parseInt(major), minor: parseInt(minor), patch: parseInt(patch) };
}
// Compare versions
function isVersionAtLeast(current: string, required: string): boolean {
  const curr = parseVersion(current);
  const req = parseVersion(required);
  // Stage comparison (MVP < V1 < V2 < V3)
  const stageOrder = { 'MVP': 0, 'V1': 1, 'V2': 2, 'V3': 3 };
  if (stageOrder[curr.stage] < stageOrder[req.stage]) return false;
  if (stageOrder[curr.stage] > stageOrder[req.stage]) return true;
  // Numeric comparison
  if (curr.major < req.major) return false;
  if (curr.major > req.major) return true;
  if (curr.minor < req.minor) return false;
  if (curr.minor > req.minor) return true;
  return curr.patch >= req.patch;
}
Feature Flag Service
class FeatureFlagService {
  private cache: Map<string, FeatureFlag>;
  async isEnabled(
    flagName: string,
    context: {
      version?: string,
      accountId?: string,
      userId?: string
    }
  ): Promise<boolean> {
    const flag = await this.getFlag(flagName);
    if (!flag.enabled) return false;
    // Check version requirement
    if (flag.min_version && context.version) {
      if (!isVersionAtLeast(context.version, flag.min_version)) {
        return false;
      }
    }
    // Check account allowlist
    if (flag.account_allowlist.length > 0) {
      if (!context.accountId || !flag.account_allowlist.includes(context.accountId)) {
        return false;
      }
    }
    // Check rollout percentage
    if (flag.rollout_percentage < 100 && context.userId) {
      const hash = hashUserId(context.userId);
      if (hash % 100 >= flag.rollout_percentage) {
        return false;
      }
    }
    return true;
  }
}
Database Migrations
Migration Principles
Golden Rules:
- Never remove columns in the same release as behavior change
- Always add before remove (expand-contract pattern)
- Make changes backward compatible for at least 1 version
- Test rollback scenarios before production deployment
- Use feature flags to control schema-dependent features
Migration Types
Type 1: Additive Changes (Safe)
Characteristics:
- Add new tables
- Add new columns with defaults
- Add new indexes
- Add new constraints (non-blocking)
Example:
-- Safe: Adding new column with default
ALTER TABLE spaces
ADD COLUMN max_occupancy INTEGER DEFAULT 2;
-- Safe: Adding new table
CREATE TABLE event_tickets (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_id UUID NOT NULL REFERENCES events(id),
  ticket_type TEXT NOT NULL,
  price_cents INTEGER NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);
Deployment: Can be deployed anytime, no rollback needed
Type 2: Backward-Compatible Changes (Requires Planning)
Characteristics:
- Rename columns (using views)
- Change column types (with casting)
- Split tables
- Modify constraints
Example:
-- Step 1 (Deploy Week 1): Add new column
ALTER TABLE bookings ADD COLUMN guest_email TEXT;
-- Step 2 (Deploy Week 1): Backfill data
UPDATE bookings SET guest_email = guests.email
FROM guests WHERE bookings.guest_id = guests.id;
-- Step 3 (Deploy Week 2): Add NOT NULL after backfill
ALTER TABLE bookings ALTER COLUMN guest_email SET NOT NULL;
-- Step 4 (Deploy Week 3): Drop old foreign key
-- Only after confirming all code uses new column
ALTER TABLE bookings DROP COLUMN guest_id;
Deployment: Multi-week rollout with validation between steps
Type 3: Breaking Changes (Avoid or Delay)
Characteristics:
- Remove columns still in use
- Change primary keys
- Remove tables
- Change data types incompatibly
Mitigation:
-- Instead of dropping column immediately, deprecate it
ALTER TABLE spaces
ADD COLUMN property_type_v2 TEXT; -- New enum values
-- Backfill
UPDATE spaces
SET property_type_v2 = CASE
  WHEN property_type = 'villa' THEN 'luxury_villa'
  ELSE property_type
END;
-- Deploy code using property_type_v2
-- Wait 1 version cycle (4-8 weeks)
-- Then drop old column
ALTER TABLE spaces DROP COLUMN property_type;
Expand-Contract Pattern
Phase 1: EXPAND
├─ Add new schema elements
├─ Dual-write to old and new schemas
└─ Feature flag controls which is read
Phase 2: MIGRATE
├─ Backfill historical data
├─ Validate data consistency
└─ Monitor error rates
Phase 3: CONTRACT
├─ Switch reads to new schema
├─ Stop writes to old schema
└─ Remove old schema elements (after grace period)
Migration Naming Convention
YYYYMMDD_HHMMSS_<version>_<description>.sql
Examples:
20250125_103000_mvp01_add_booking_tables.sql
20250301_141500_mvp02_add_channel_configs.sql
20250615_090000_v10_add_payment_tables.sql
Rolling Deployment Migration Strategy
// Migration metadata
interface Migration {
  version: string;
  timestamp: number;
  type: 'additive' | 'backward_compatible' | 'breaking';
  requires_downtime: boolean;
  rollback_safe: boolean;
}
// Pre-deployment validation
async function validateMigration(migration: Migration) {
  // Check if old code can run with new schema
  if (migration.type === 'breaking' && !migration.requires_downtime) {
    throw new Error('Breaking changes require downtime or multi-phase migration');
  }
  // Verify rollback safety
  if (!migration.rollback_safe) {
    console.warn('⚠️  Migration cannot be rolled back safely');
  }
}
API Versioning
Versioning Strategy
TVL uses URL-based versioning with header-based overrides for maximum clarity and client control.
URL-Based Versioning
Base URL Pattern:
https://api.tvl.com/{version}/{resource}
Examples:
GET  https://api.tvl.com/v1/spaces
POST https://api.tvl.com/v1/bookings
GET  https://api.tvl.com/v2/events
Route Structure:
// Express.js example
app.use('/v1', v1Router);
app.use('/v2', v2Router);
app.use('/v3', v3Router);
// Version-specific controllers
v1Router.get('/spaces', v1SpacesController.list);
v2Router.get('/spaces', v2SpacesController.list); // Enhanced with events
Header-Based Version Override
GET /spaces HTTP/1.1
Host: api.tvl.com
X-API-Version: v2
X-App-Version: V2.1.3.0
Precedence:
- URL version (highest priority)
- X-API-Versionheader
- Default to latest stable version
API Version Lifecycle
| Version | Status | Support Level | End-of-Life | 
|---|---|---|---|
| v1 | Current | Full support | V3.0 launch + 6 months | 
| v2 | Beta | Limited support | V4.0 launch (if applicable) | 
| v3 | Alpha | Experimental | N/A | 
Version Mapping to Product Stages
| Product Version | API Version | Features | 
|---|---|---|
| MVP.0 - MVP.2 | v1 (alpha) | Spaces, units, Hostaway sync | 
| V1.0 - V1.x | v1 (stable) | + Direct bookings, payments | 
| V2.0 - V2.x | v2 (stable) | + Events, experiences | 
| V3.0 - V3.x | v3 (stable) | + Marketplace, multi-vendor | 
Breaking Change Policy
Definition of Breaking Change:
- Removing endpoints
- Removing request/response fields
- Changing field types
- Changing authentication requirements
- Changing rate limits (downward)
- Changing error codes
Migration Path:
- Announce deprecation (6 months notice)
- Add deprecation headers to responses
- Provide migration guide
- Support old version for 6 months post-deprecation
- Remove old version
Deprecation Headers:
HTTP/1.1 200 OK
Deprecation: Sun, 11 Jun 2026 23:59:59 GMT
Sunset: Sun, 11 Dec 2026 23:59:59 GMT
Link: <https://docs.tvl.com/api/v2/migration>; rel="deprecation"
Non-Breaking Change Examples
Safe to add:
- New endpoints
- New optional request parameters
- New response fields
- New HTTP methods on existing resources
Example of backward-compatible enhancement:
// V1 response
{
  "space_id": "sp_123",
  "name": "Sunset Villa"
}
// V2 response (adds fields, doesn't remove)
{
  "space_id": "sp_123",
  "name": "Sunset Villa",
  "events_enabled": true,  // NEW
  "max_event_capacity": 50  // NEW
}
Version Negotiation
function selectAPIVersion(req: Request): string {
  // 1. Check URL path
  const pathMatch = req.path.match(/^\/(v\d+)\//);
  if (pathMatch) return pathMatch[1];
  // 2. Check header
  const headerVersion = req.headers['x-api-version'];
  if (headerVersion && isValidVersion(headerVersion)) {
    return headerVersion;
  }
  // 3. Default to client's app version mapping
  const appVersion = req.headers['x-app-version'];
  if (appVersion) {
    return mapAppVersionToAPIVersion(appVersion);
  }
  // 4. Fall back to latest stable
  return 'v1';
}
function mapAppVersionToAPIVersion(appVersion: string): string {
  const { stage } = parseVersion(appVersion);
  switch (stage) {
    case 'MVP': return 'v1';
    case 'V1': return 'v1';
    case 'V2': return 'v2';
    case 'V3': return 'v3';
    default: return 'v1';
  }
}
Client Compatibility
Minimum Supported Versions
| Client Platform | Minimum Version | Released | Support End | 
|---|---|---|---|
| Web App | V1.0.0 | 2025-07 | Rolling 6-month window | 
| iOS App | V1.0.0 | 2025-08 | Rolling 12-month window | 
| Android App | V1.0.0 | 2025-08 | Rolling 12-month window | 
| API Clients | v1 | 2025-07 | Until v1 sunset | 
Version Compatibility Matrix
| Server Version | Compatible Client Versions | 
|---|---|
| MVP.0 | MVP.0 only | 
| MVP.1 | MVP.0, MVP.1 | 
| MVP.2 | MVP.0, MVP.1, MVP.2 | 
| V1.0 | MVP.2+, V1.x | 
| V2.0 | V1.0+, V2.x | 
| V3.0 | V2.0+, V3.x | 
Graceful Degradation Strategy
Feature Availability Check
Client-side:
// Check server capabilities
const serverVersion = await getServerVersion();
if (isVersionAtLeast(serverVersion, 'V2.0.0.0')) {
  // Show events UI
  renderEventsFeature();
} else {
  // Hide events UI gracefully
  console.log('Events not available in this version');
}
Server Response Adaptation
Server-side:
app.get('/api/v1/spaces/:id', async (req, res) => {
  const clientVersion = req.headers['x-app-version'] || 'MVP.0.0.0';
  const space = await getSpace(req.params.id);
  // Full response for V2+ clients
  if (isVersionAtLeast(clientVersion, 'V2.0.0.0')) {
    return res.json({
      ...space,
      events_enabled: space.events_enabled,
      upcoming_events: await getUpcomingEvents(space.id)
    });
  }
  // Limited response for older clients
  return res.json({
    id: space.id,
    name: space.name,
    description: space.description
    // Omit events-related fields
  });
});
Client Update Prompts
Soft Prompt (Recommended Update)
// Server includes update hint in response
{
  "data": { ... },
  "meta": {
    "server_version": "V2.1.0.0",
    "client_version": "V1.2.0.0",
    "update_available": true,
    "update_url": "https://tvl.com/download",
    "new_features": ["Events management", "Enhanced search"]
  }
}
Hard Requirement (Forced Update)
// Server returns 426 Upgrade Required
HTTP/1.1 426 Upgrade Required
Content-Type: application/json
{
  "error": "client_version_too_old",
  "message": "This version is no longer supported. Please update.",
  "minimum_version": "V1.0.0.0",
  "current_version": "MVP.0.5.0",
  "update_url": "https://tvl.com/download"
}
Version Deprecation Timeline
T+0:    New version released
T+3mo:  Deprecation warning added to old version
T+6mo:  Old version marked as unsupported
T+9mo:  Hard requirement to upgrade
T+12mo: Old version API shutdown
Deployment Strategy
Blue-Green Deployment
Architecture
                    Load Balancer
                         |
                    +----+----+
                    |         |
                Blue Pool  Green Pool
                (V1.0)     (V1.1)
                    |         |
              +-----+-----+   +
              |     |     |
            Pod1  Pod2  Pod3 (Inactive)
Deployment Process
Step 1: Prepare Green Environment
# Deploy new version to green pool (inactive)
kubectl apply -f k8s/deployment-v1.1-green.yaml
# Wait for pods to be ready
kubectl wait --for=condition=ready pod -l version=v1.1 --timeout=300s
Step 2: Smoke Test Green
# Run health checks
curl https://green.tvl.internal/health
curl https://green.tvl.internal/api/v1/spaces
# Run integration tests against green
npm run test:integration -- --target=green
Step 3: Switch Traffic
# Update load balancer to point to green
kubectl patch service api-service -p '{"spec":{"selector":{"version":"v1.1"}}}'
# Monitor metrics for 15 minutes
datadog monitor --alert-on-error-spike
Step 4: Keep Blue as Rollback
# Keep blue running for 24 hours
# If no issues, decommission blue
kubectl delete -f k8s/deployment-v1.0-blue.yaml
Canary Releases
Gradual Rollout
Phase 1: 5% traffic  → Monitor for 2 hours
Phase 2: 25% traffic → Monitor for 4 hours
Phase 3: 50% traffic → Monitor for 8 hours
Phase 4: 100% traffic → Monitor for 24 hours
Traffic Splitting
# Istio VirtualService example
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api-canary
spec:
  hosts:
    - api.tvl.com
  http:
    - match:
        - headers:
            x-canary-user:
              exact: "true"
      route:
        - destination:
            host: api-service
            subset: v1.1
    - route:
        - destination:
            host: api-service
            subset: v1.0
          weight: 95
        - destination:
            host: api-service
            subset: v1.1
          weight: 5
Canary Metrics
Auto-rollback triggers:
- Error rate > 1% (baseline + 0.5%)
- p95 latency > 500ms (baseline + 100ms)
- 5xx responses > 10/minute
- Database connection failures > 5/minute
// Automated canary analysis
async function evaluateCanary(canaryVersion: string) {
  const metrics = await getMetrics(canaryVersion, duration: '1h');
  if (metrics.errorRate > 0.01) {
    await rollback(canaryVersion);
    alert('Canary rolled back due to error rate');
    return false;
  }
  if (metrics.p95Latency > 500) {
    await rollback(canaryVersion);
    alert('Canary rolled back due to latency');
    return false;
  }
  return true;
}
Database Migration During Deployment
#!/bin/bash
# deploy.sh
set -e
echo "Starting deployment of v1.1..."
# 1. Run additive migrations (safe to run with old code)
echo "Running database migrations..."
npm run migrate:up
# 2. Deploy new code to green pool
echo "Deploying to green pool..."
kubectl apply -f k8s/green-v1.1.yaml
kubectl wait --for=condition=ready pod -l pool=green
# 3. Run smoke tests
echo "Running smoke tests..."
npm run test:smoke -- --target=green
# 4. Switch traffic
echo "Switching traffic to green..."
kubectl patch service api -p '{"spec":{"selector":{"pool":"green"}}}'
# 5. Monitor for issues
echo "Monitoring for 15 minutes..."
sleep 900
# 6. Check metrics
if ! check_metrics; then
  echo "Metrics failed, rolling back..."
  kubectl patch service api -p '{"spec":{"selector":{"pool":"blue"}}}'
  exit 1
fi
echo "Deployment successful!"
Rollback Procedures
Instant Rollback (Traffic Switch)
# Revert load balancer to blue pool
kubectl patch service api-service -p '{"spec":{"selector":{"version":"v1.0"}}}'
# Verify rollback
curl https://api.tvl.com/health | jq .version
# Expected: "v1.0"
Database Rollback (If Required)
-- Rollback migration (only if additive)
-- migrations/20250615_v11_add_events.down.sql
DROP TABLE IF EXISTS event_tickets;
DROP TABLE IF EXISTS events;
Rollback decision tree:
Is migration additive-only?
├─ YES: Keep new schema, rollback code only
└─ NO: Was data written to new schema?
    ├─ YES: Cannot rollback DB, must fix forward
    └─ NO: Safe to rollback DB migration
Communication Protocol
Incident Response:
- Detect issue (automated or manual)
- Assess impact and scope
- Decision: Fix forward or rollback?
- Execute rollback procedure
- Post-mortem analysis
Notification channels:
// Rollback initiated
await slack.send('#engineering-alerts', {
  text: '🚨 Rollback initiated for v1.1 deployment',
  severity: 'high',
  reason: 'Error rate exceeded threshold',
  action: 'Reverting to v1.0'
});
// Email leadership
await email.send('engineering-leads@tvl.com', {
  subject: 'Production rollback: v1.1',
  body: rollbackReport
});
Version Lifecycle Management
Version Support Timeline
Version Release
      |
      |---- 3 months: Active Development
      |---- 6 months: Maintenance Mode
      |---- 9 months: Security Fixes Only
      |---- 12 months: End of Life
Deprecation Process
6 Months Before EOL:
- Add deprecation warnings to API responses
- Email customers using deprecated version
- Update documentation with migration guide
3 Months Before EOL:
- Increase warning frequency
- Require acknowledgment of deprecation
- Offer migration assistance
1 Month Before EOL:
- Final warning emails
- Dashboard notices for affected users
- Block new users from choosing deprecated version
EOL Date:
- Disable API endpoints for deprecated version
- Redirect to latest version documentation
- Maintain read-only access for 30 days (data export)
Testing Strategy
Version Compatibility Testing
// Test matrix
const versionPairs = [
  { client: 'MVP.0', server: 'MVP.0', expected: 'full' },
  { client: 'MVP.0', server: 'MVP.1', expected: 'degraded' },
  { client: 'V1.0', server: 'V2.0', expected: 'degraded' },
  { client: 'V2.0', server: 'V2.0', expected: 'full' },
];
describe('Version Compatibility', () => {
  versionPairs.forEach(({ client, server, expected }) => {
    it(`${client} client with ${server} server`, async () => {
      const response = await apiCall({ version: client });
      if (expected === 'full') {
        expect(response).toHaveAllFeatures();
      } else {
        expect(response).toHaveCoreFeatures();
      }
    });
  });
});
Migration Testing
# Test upgrade path
npm run test:migration -- --from=v1.0 --to=v1.1
# Test rollback safety
npm run test:rollback -- --from=v1.1 --to=v1.0
Summary
This versioning strategy ensures:
- Zero-downtime deployments through blue-green and canary releases
- Backward compatibility via feature flags and API versioning
- Safe database migrations using expand-contract pattern
- Graceful client degradation for older versions
- Clear deprecation process with 12-month support lifecycle
Key Takeaways:
- Always use feature flags for new capabilities
- Never break backward compatibility within a major version
- Use additive-only database migrations
- Support at least 2 API versions simultaneously
- Follow the expand-contract pattern for schema changes