Skip to main content

TVL Platform - Product Versioning Strategy

Last Updated: 2025-10-25 Status: Active Owner: Engineering & Product Leadership


Table of Contents

  1. Overview
  2. Version Structure
  3. Feature Flags
  4. Database Migrations
  5. API Versioning
  6. Client Compatibility
  7. Deployment Strategy
  8. Version Lifecycle Management
  9. Testing Strategy
  10. 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

  1. Additive-Only Migrations: Database changes must never break existing code
  2. Feature Flag Everything: All new features gated by runtime flags
  3. API Compatibility: Maintain at least 2 major API versions simultaneously
  4. Client Graceful Degradation: Older clients must continue functioning
  5. 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

StagePurposeDurationStability
MVP.0Foundation & proof of concept3 monthsExperimental
MVP.1Core functionality validation2 monthsBeta
MVP.2Pre-production stabilization2 monthsRelease Candidate
V1.xProduction-ready platform6 monthsStable
V2.xFeature expansion6 monthsStable
V3.xEnterprise scale6 monthsStable

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:

  1. Never remove columns in the same release as behavior change
  2. Always add before remove (expand-contract pattern)
  3. Make changes backward compatible for at least 1 version
  4. Test rollback scenarios before production deployment
  5. 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:

  1. URL version (highest priority)
  2. X-API-Version header
  3. Default to latest stable version

API Version Lifecycle

VersionStatusSupport LevelEnd-of-Life
v1CurrentFull supportV3.0 launch + 6 months
v2BetaLimited supportV4.0 launch (if applicable)
v3AlphaExperimentalN/A

Version Mapping to Product Stages

Product VersionAPI VersionFeatures
MVP.0 - MVP.2v1 (alpha)Spaces, units, Hostaway sync
V1.0 - V1.xv1 (stable)+ Direct bookings, payments
V2.0 - V2.xv2 (stable)+ Events, experiences
V3.0 - V3.xv3 (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:

  1. Announce deprecation (6 months notice)
  2. Add deprecation headers to responses
  3. Provide migration guide
  4. Support old version for 6 months post-deprecation
  5. 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 PlatformMinimum VersionReleasedSupport End
Web AppV1.0.02025-07Rolling 6-month window
iOS AppV1.0.02025-08Rolling 12-month window
Android AppV1.0.02025-08Rolling 12-month window
API Clientsv12025-07Until v1 sunset

Version Compatibility Matrix

Server VersionCompatible Client Versions
MVP.0MVP.0 only
MVP.1MVP.0, MVP.1
MVP.2MVP.0, MVP.1, MVP.2
V1.0MVP.2+, V1.x
V2.0V1.0+, V2.x
V3.0V2.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

// 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:

  1. Detect issue (automated or manual)
  2. Assess impact and scope
  3. Decision: Fix forward or rollback?
  4. Execute rollback procedure
  5. 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:

  1. Always use feature flags for new capabilities
  2. Never break backward compatibility within a major version
  3. Use additive-only database migrations
  4. Support at least 2 API versions simultaneously
  5. Follow the expand-contract pattern for schema changes