Skip to content

Scalability Guide

Enterprise scalability patterns for global deployment

Current Stack (February 2026)

LayerTechnologyVersion
FrontendNext.js (Azure Static Web Apps)16.1.6
BackendNestJS (Azure Container Apps)11.x
DatabaseMongoDB Atlas7.x
StorageAzure Blob Storage-
CDNAzure Front Door (deployed)-
CI/CDGitHub Actions, Node.js 22-

Current Architecture

┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│   Frontend   │────▶│  Azure Container │────▶│   MongoDB    │
│  (Static)    │     │  Apps (Backend)  │     │   Atlas      │
└──────────────┘     └──────────────────┘     └──────────────┘

                     ┌────────┴────────┐
                     ▼                 ▼
              ┌────────────┐   ┌────────────────┐
              │ Azure Blob │   │   Azure CDN    │
              │  Storage   │   │   (Optional)   │
              └────────────┘   └────────────────┘

Horizontal Scaling

Container Apps Auto-scaling

Azure Container Apps supports automatic scaling:

bash
az containerapp update \
  --name ctrl-audio-backend \
  --resource-group Sonnance-WebApp \
  --min-replicas 1 \
  --max-replicas 10 \
  --scale-rule-name http-rule \
  --scale-rule-type http \
  --scale-rule-http-concurrency 100

Scaling Triggers

MetricRecommendation
HTTP RequestsScale at 100 concurrent connections
MemoryScale at 70% memory utilization
Queue DepthFor background jobs (future)

Database Optimization

MongoDB Indexes

Ensure indexes exist for common queries:

javascript
// Artist Space queries
db.artistspaces.createIndex({ owner: 1 });
db.artistspaces.createIndex({ "collaborators.user": 1 });

// Track queries
db.tracks.createIndex({ project: 1, isDeleted: 1 });
db.tracks.createIndex({ owner: 1, createdAt: -1 });

// Search optimization
db.artistspaces.createIndex({ name: "text" });
db.projects.createIndex({ name: "text" });
db.tracks.createIndex({ name: "text" });

Aggregation Pipeline Optimization

typescript
// Use $project early to reduce document size
const pipeline = [
  { $match: { owner: userId, isDeleted: false } },
  { $project: { name: 1, image: 1, updatedAt: 1 } }, // Early projection
  { $sort: { updatedAt: -1 } },
  { $limit: 50 }
];

CDN for Global Distribution

Azure CDN Setup

Run the setup script:

bash
cd infrastructure
chmod +x setup-cdn.sh
./setup-cdn.sh

CDN Benefits

MetricWithout CDNWith CDN
Latency (EU)50-100ms10-20ms
Latency (US)150-200ms20-30ms
Bandwidth costs$0.087/GB$0.081/GB
Cache hit ratio0%85-95%

CDN URL Integration

typescript
// Environment variable
NEXT_PUBLIC_CDN_URL=https://sonnance-images.azureedge.net

// Replace storage URLs with CDN
const cdnUrl = imageUrl.replace(
  'sonnancewebappstorage00.blob.core.windows.net',
  process.env.CDN_HOSTNAME
);

Mobile App API Considerations

Pagination

Always use cursor-based pagination for mobile:

typescript
// Cursor-based (recommended)
GET /tracks?cursor=507f1f77bcf86cd799439011&limit=20

// Response
{
  items: [...],
  nextCursor: "507f1f77bcf86cd799439012",
  hasMore: true
}

Response Compression

Enable gzip compression:

typescript
import * as compression from 'compression';
app.use(compression());

Efficient Endpoints for Mobile

typescript
// ✅ Good - Single request with embedded data
GET /artist-space/:id/full
// Returns: space + projects + recent tracks

// ❌ Avoid - Multiple round trips
GET /artist-space/:id
GET /projects?spaceId=xxx
GET /tracks?projectId=xxx

Image Optimization for Mobile

Return appropriate image variants:

typescript
// Mobile clients request smaller images
GET /artist/:id?imageSize=thumbnail

// Responsive image URLs in responses
{
  image: {
    thumbnail: "https://cdn/.../thumb.webp",  // 150x150
    medium: "https://cdn/.../medium.webp",    // 600px
    original: "https://cdn/.../original.webp"
  }
}

Caching Strategy

API Response Caching

typescript
// Redis caching (recommended for future)
import { CacheModule } from '@nestjs/cache-manager';
import * as redisStore from 'cache-manager-redis-store';

CacheModule.register({
  store: redisStore,
  host: 'localhost',
  ttl: 300, // 5 minutes
});

Cache Invalidation

Invalidate on mutations:

typescript
@Injectable()
export class ArtistService {
  async update(id: string, dto: UpdateDto) {
    await this.cacheManager.del(`artist:${id}`);
    return this.repository.update(id, dto);
  }
}

Background Jobs

Current: @nestjs/schedule

typescript
// Trash cleanup runs periodically
@Cron('0 0 * * *') // Daily at midnight
async deleteOldItems() {
  // Delete items older than 30 days
}

Future: Dedicated Queue (BullMQ)

For heavy processing (audio transcoding, batch operations):

typescript
import { BullModule } from '@nestjs/bull';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'audio-processing',
    }),
  ],
})

Monitoring & Observability

Azure Application Insights

typescript
import * as appInsights from 'applicationinsights';

appInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY)
  .setAutoCollectRequests(true)
  .setAutoCollectDependencies(true)
  .start();

Health Checks

typescript
import { HealthModule, HealthCheckService } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('mongodb'),
      () => this.storage.pingCheck('azure-blob'),
    ]);
  }
}

Scaling Roadmap

PhaseFeatureEffortImpact
1Azure CDN1 hourHigh
2MongoDB indexes2 hoursHigh
3Response compression30 minMedium
4Application Insights2 hoursMedium
5Redis caching1 dayHigh
6BullMQ for jobs2 daysMedium

Global Scaling Strategy — Multi-Region, Multi-Continent

Added: March 2026 Context: Sonnance targets the global music industry (200+ countries). Competition like Hanteo Global already operates at 15M+ users across 40,000+ cities. This section ensures Sonnance's architecture can scale to that level with low admin overhead, acceptable latency worldwide, and compliance with data sovereignty regulations.

Why Global Architecture Matters Early

Even before we need it, architectural decisions made now determine how painful (or painless) global scaling will be later. Choosing the wrong database replication strategy or storage topology today means a re-architecture later.

THE LATENCY PROBLEM — SINGLE REGION (CURRENT)
═══════════════════════════════════════════════════════

  West Europe (current)
  ┌──────────────────┐
  │ Backend + DB     │
  │ + Storage        │
  │ West Europe      │
  └──────┬───────────┘

    Latency from user:
    ├── Europe:         10-30ms  ✅ Excellent
    ├── US East:        80-120ms ✅ Acceptable
    ├── US West:       140-180ms ⚠️ Noticeable
    ├── Latin America: 150-200ms ⚠️ Noticeable
    ├── East Asia:     250-350ms ❌ Poor (Korea, Japan)
    ├── Southeast Asia:300-400ms ❌ Poor
    └── Oceania:       350-450ms ❌ Poor

  Audio streaming adds buffering on top of API latency.
  A user in Seoul (key K-POP market) waits 300ms+
  for every API call — comments feel sluggish,
  player controls lag, real-time collaboration breaks.

Multi-Region Architecture (Target State)

GLOBAL ARCHITECTURE — MULTI-REGION
═══════════════════════════════════════════════════════

                    ┌───────────────────────┐
                    │   Azure Front Door    │
                    │   (Global load        │
                    │    balancer + CDN)     │
                    └─────────┬─────────────┘

              ┌───────────────┼───────────────┐
              │               │               │
    ┌─────────▼──────┐ ┌─────▼──────┐ ┌──────▼─────────┐
    │  REGION: EU    │ │  REGION:   │ │  REGION:       │
    │  West Europe   │ │  Americas  │ │  Asia-Pacific  │
    │                │ │  East US 2 │ │  Korea Central │
    │ ┌────────────┐ │ │ ┌────────┐ │ │ ┌────────────┐ │
    │ │ Container  │ │ │ │ Cont.  │ │ │ │ Container  │ │
    │ │ Apps       │ │ │ │ Apps   │ │ │ │ Apps       │ │
    │ └──────┬─────┘ │ │ └───┬────┘ │ │ └──────┬─────┘ │
    │        │       │ │     │      │ │        │       │
    │ ┌──────▼─────┐ │ │ ┌───▼────┐ │ │ ┌──────▼─────┐ │
    │ │ Redis      │ │ │ │ Redis  │ │ │ │ Redis      │ │
    │ │ (cache)    │ │ │ │ (cache)│ │ │ │ (cache)    │ │
    │ └────────────┘ │ │ └────────┘ │ │ └────────────┘ │
    │ ┌────────────┐ │ │ ┌────────┐ │ │ ┌────────────┐ │
    │ │ Blob       │ │ │ │ Blob   │ │ │ │ Blob       │ │
    │ │ Storage    │ │ │ │ Storage│ │ │ │ Storage    │ │
    │ └────────────┘ │ │ └────────┘ │ │ └────────────┘ │
    └────────────────┘ └────────────┘ └────────────────┘
              │               │               │
              └───────────────┼───────────────┘

                    ┌─────────▼─────────────┐
                    │   MongoDB Atlas       │
                    │   Global Cluster      │
                    │   (multi-region       │
                    │    read replicas)     │
                    └───────────────────────┘

Technology Choices for Global Scale

1. Compute — Azure Container Apps (stay, but multi-region)

AspectCurrentGlobal TargetWhy
Regions1 (West Europe)3 (EU + Americas + APAC)Cover 90%+ of global users within 100ms
Auto-scaling1-10 replicas1-10 per regionIndependent scaling per traffic pattern
DeploymentSingleBlue-green per regionZero-downtime deploys, regional rollback
Admin effortLowLow (IaC)Bicep/Terraform templates replicate across regions

Why stay with Container Apps (not Kubernetes):

  • Consumption-based pricing — pay only for requests, zero cost when idle
  • Built-in scale-to-zero — perfect for regions that may have low traffic initially
  • No cluster management — Azure handles the container orchestration
  • Dapr integration — built-in service-to-service invocation, state management, pub/sub across regions

When to re-evaluate: Only if we need GPU workloads (AI inference at the edge), custom networking (VNET peering), or exceed Container Apps limits (~100 replicas). At that point, AKS (Azure Kubernetes Service) becomes the path.

2. Database — MongoDB Atlas Global Clusters

ApproachHow It WorksLatencyCostAdmin EffortBest For
Single region (current)One M0 cluster in AWS EU10ms EU, 300ms APACFree (M0)NoneMVP, < 1,000 users
Multi-region read replicasPrimary in EU, secondaries in US + APAC< 50ms reads everywhereM10+ (~$57/mo base + replica cost)Low1K-100K users
Global Clusters (Zone Sharding)Data pinned to regions by user location, reads always local< 20ms reads, < 50ms writesM30+ (~$500/mo+)Medium100K+ users, data sovereignty
Atlas Edge ServerMongoDB Sync to edge devices/regions< 10ms readsPremium pricingMedium-HighMobile-first, offline-first

Recommended path:

  1. Now → Stay on M0 (free), single region. Write reads and writes to EU.
  2. 1K-10K users → Upgrade to M10, add read replicas in US East + Korea Central.
  3. 10K-100K users → Atlas Global Cluster with zone sharding. Users' data lives in their closest region.
  4. 100K+ users → Evaluate whether MongoDB Atlas still makes sense vs. Azure Cosmos DB for MongoDB vCore (native Azure, global distribution built-in).

Zone sharding example (when we need it):

javascript
// MongoDB Atlas Zone Sharding — data localized by region
// Shard key: { region: 1, owner: 1 }

// User signs up from Seoul → data goes to APAC shard
{
  _id: ObjectId("..."),
  username: "min_ji",
  region: "apac",         // ← determines shard placement
  email: "minji@example.kr"
}

// User signs up from Buenos Aires → data goes to Americas shard
{
  _id: ObjectId("..."),
  username: "facundo",
  region: "americas",     // ← determines shard placement
  email: "f@example.com"
}

// Cross-region collaboration: user in Seoul works with user in Madrid
// → Both read from local replicas (fast reads)
// → Writes go to the region of the resource owner (one write hop)

3. Storage — Azure Blob (replicated) + Front Door

StrategyHow It WorksLatencyCostAdmin
GRS (Geo-Redundant)Automatic replication to paired regionSame origin latency, failover available2x LRS cost (~$0.036/GB)None
GZRSGRS + zone redundancyBest durability~$0.046/GBNone
Multi-account + Front DoorSeparate storage accounts per region, CDN serves nearest< 20ms (cached)Per-account cost + Front DoorLow
Azure Blob + Akamai/CloudflareSingle origin, CDN edge serves globally< 30ms (cached)CDN feesLow

Recommended path for audio files:

  1. Now → Single storage + Front Door (deployed). CDN caches images/audio at edge.
  2. 10K+ users → Add storage accounts in US + APAC. Upload goes to nearest region. Front Door routes requests to nearest origin.
  3. Audio-specific: Large WAV files (50-100MB) benefit from Azure Blob edge zones — files uploaded to nearest edge, replicated in background.

Key insight: Audio files are write-once, read-many. A track uploaded in Madrid will be played by collaborators in Seoul and NYC. The CDN handles this well — first play from a new region incurs origin fetch latency, subsequent plays are cached.

4. Real-Time — WebSocket (Socket.io) Global

ChallengeSolutionTechnology
WebSocket connections must be stickyAzure Container Apps supports sticky sessions via ARR AffinityBuilt-in
Cross-region pub/subRedis pub/sub adapter for Socket.io — messages flow between regionsAzure Cache for Redis with geo-replication
Presence across regionsShared Redis stores online status, any region can read itRedis geo-replicated
Connection re-routing on failoverFront Door health probes detect region down → re-route connectionsAzure Front Door
WEBSOCKET GLOBAL ARCHITECTURE
──────────────────────────────────────────

  User in Seoul connects to APAC region:
    Socket.io → Container Apps (APAC) → Redis (APAC)

  User in Madrid connects to EU region:
    Socket.io → Container Apps (EU) → Redis (EU)

  Seoul user comments on Madrid user's track:
    1. Comment saved to DB (write to primary)
    2. Event published to Redis (APAC)
    3. Redis geo-replication → Redis (EU)
    4. Socket.io adapter (EU) picks up event
    5. Madrid user receives comment in real-time
    
  Total latency: ~100-150ms (acceptable for comments)
  For playback sync: consider region-local rooms only

5. CDN — Azure Front Door (expand configuration)

Already deployed (sonnance-cdn). For global scale:

ImprovementWhatWhen
Add origins per regionStorage accounts in US + APAC as additional origins10K+ users
Custom rulesRoute audio vs. image vs. API traffic differentlyNow (optimize cache TTLs)
WAF (Web Application Firewall)DDoS protection, bot mitigation, geo-filteringBefore public launch
Private Link originsConnect Front Door to storage via private networkEnterprise tier
Audio-specific cachingLonger TTL for audio (immutable once uploaded), shorter for profile imagesNow

Data Sovereignty & Compliance

Music platforms operate globally but data laws are local. Plan for this before it becomes a blocker.

RegionKey RegulationWhat It Means for SonnanceRequired By
EUGDPRUser data stays in EU unless user consents. Right to delete, portability, DPO.Now (existing users)
South KoreaPIPA (Personal Information Protection Act)Stricter than GDPR on cross-border transfers. Local storage preferred. Consent for international transfer.Before K-POP market push
BrazilLGPDGDPR-equivalent. Data processing consent. Local DPO representative.Before LATAM expansion
US (California)CCPA/CPRAConsumer data rights, opt-out of sale, retention limits. No federal law yet.Before US growth
ChinaPIPL + CSLData localization mandatory. Cross-border transfer requires security assessment.Only if entering Chinese market
JapanAPPIConsent for cross-border. Adequacy framework with EU.Before Japan expansion
IndiaDPDP Act (2023)Consent-based, data fiduciary obligations.Before India expansion

Architecture implications:

DATA SOVEREIGNTY MODEL
═══════════════════════════════════════════════════════

  APPROACH 1: ZONE SHARDING (Recommended)
  ──────────────────────────────────────────
  • User data physically stays in their region
  • MongoDB Atlas zone sharding pins documents to geographic zones
  • Cross-region collaboration: metadata replicated,
    personal data stays local
  • Compliant by default — no cross-border transfer of user PII

  APPROACH 2: DATA RESIDENCY FLAG (Simpler, less compliant)
  ──────────────────────────────────────────
  • All data in EU (current)
  • Flag per user: "data_residency: eu|us|apac"
  • On request, migrate user's data to their region
  • Simpler to implement, harder to guarantee compliance

  APPROACH 3: SOVEREIGN CLOUD (Enterprise only)
  ──────────────────────────────────────────
  • Dedicated deployment per country/region
  • Completely isolated: compute, storage, database
  • Maximum compliance, maximum cost
  • Only for regulated enterprise clients (government, military)

  RECOMMENDED: Start with Approach 2 (flag-based), evolve to
  Approach 1 (zone sharding) when entering regulated markets.

Region Priority & Rollout Plan

Not all regions are equal. Prioritize by market size, latency impact, and strategic opportunity.

PriorityRegionAzure RegionWhyLatency GainEstimated Users
P0 (current)EuropeWest Europe (Netherlands)Home base, EU artists/labelsBaseline1K-10K
P1AmericasEast US 2 (Virginia)Largest music market, Latin America bridgeEU→US: 300ms→20ms10K-50K
P2Asia-PacificKorea Central (Seoul)K-POP is $10B+ industry, Hanteo validates demandEU→KR: 350ms→20ms5K-30K
P3Latin AmericaBrazil South (São Paulo)350M+ Spanish/Portuguese speakers, massive music cultureEU→BR: 250ms→20ms5K-20K
P4Japan/SEAJapan East (Tokyo)J-POP market, SEA growingEU→JP: 300ms→20ms3K-15K
P5Middle East/AfricaUAE North (Dubai)Emerging music tech marketEU→UAE: 150ms→20ms1K-5K
ROLLOUT TIMELINE
═══════════════════════════════════════════════════════

  2026 H1: West Europe only (current)
  ├── CDN serves all regions (acceptable for static content)
  ├── API latency is the bottleneck for APAC/Americas
  └── Focus: product-market fit, not global infra

  2026 H2: Add East US 2
  ├── Americas API latency: 300ms → 20ms
  ├── LATAM improvement: 250ms → 80ms
  ├── Trigger: 20%+ of traffic from Americas
  └── Effort: 1-2 weeks (replicate Container Apps + Redis)

  2027 H1: Add Korea Central
  ├── APAC API latency: 350ms → 20ms
  ├── Strategic for K-POP market entry
  ├── Trigger: Partnership with Korean label/artist OR
  │   10%+ traffic from APAC
  └── Effort: 1-2 weeks (same IaC templates)

  2027 H2: Add Brazil South (if LATAM growth warrants)
  ├── LATAM API latency: 200ms → 20ms
  ├── LGPD compliance built-in
  └── Trigger: 15%+ traffic from LATAM

  2028+: Add Japan East, UAE North as demand requires

Low Admin Effort: Infrastructure as Code

The entire multi-region setup must be reproducible with one command. Zero manual configuration.

TechnologyPurposeWhy
Bicep / TerraformDefine all Azure resources as codeOne template → deploy to any region
GitHub ActionsCI/CD per regionPush to main → deploy to all active regions
Azure Front DoorGlobal load balancing + failoverAutomatic health checks, traffic routing
MongoDB Atlas APIDatabase region managementTerraform provider for Atlas cluster configuration
Helm charts (future, if AKS)Container orchestration templatesOnly if Container Apps limits are reached
bash
# EXAMPLE: Deploy new region (IaC approach)

# 1. Define new region in configuration
echo 'regions: [westeurope, eastus2, koreacentral]' > regions.yml

# 2. Run Bicep deployment for new region
az deployment sub create \
  --location koreacentral \
  --template-file main.bicep \
  --parameters region=koreacentral tier=standard

# 3. Configure MongoDB Atlas replica
# (via Terraform Atlas provider or Atlas Admin API)
atlas clusters update Cluster00 \
  --replicationSpecs '[
    {"zoneName":"EU","regionConfigs":[{"regionName":"EU_WEST_1","priority":7,"electableSpecs":{"instanceSize":"M10"}}]},
    {"zoneName":"US","regionConfigs":[{"regionName":"US_EAST_2","priority":6,"readOnlySpecs":{"instanceSize":"M10"}}]},
    {"zoneName":"APAC","regionConfigs":[{"regionName":"AP_NORTHEAST_2","priority":5,"readOnlySpecs":{"instanceSize":"M10"}}]}
  ]'

# 4. Update Front Door to include new origin
az afd origin create \
  --origin-group-name sonnance-origins \
  --profile-name sonnance-cdn \
  --origin-name apac-backend \
  --host-name ctrl-audio-backend-apac.koreacentral.azurecontainerapps.io \
  --priority 1 --weight 1000

# 5. Done — Front Door auto-routes APAC traffic to Seoul backend

Cost Modeling for Multi-Region

ComponentSingle Region (now)2 Regions (EU+US)3 Regions (EU+US+APAC)
Container Apps (consumption)~$0 (free tier)~$15-30/mo~$25-50/mo
MongoDB Atlas$0 (M0 free)~$114/mo (M10 x2)~$171/mo (M10 x3)
Azure Blob Storage~$1/mo~$3/mo~$5/mo
Azure Front Door~$5-10/mo~$10-20/mo~$15-30/mo
Redis Cache$0 (not yet)~$50/mo (Basic x2)~$75/mo (Basic x3)
Total~$11/mo~$192-217/mo~$291-331/mo

Key insight: Going from 1 to 3 regions costs ~$300/mo — well within startup budgets. The jump to global is surprisingly affordable with consumption-based compute. The expensive part is the database tier (M10+ for replicas). Delay this cost until traffic justifies it.

Performance Benchmarks to Target

MetricSingle RegionMulti-Region TargetHow to Measure
API p50 latency (any continent)< 50ms EU, 300ms+ elsewhere< 50ms everywhereApplication Insights
API p99 latency< 200ms EU< 150ms everywhereApplication Insights
Audio stream start< 2s EU, 5s+ APAC< 2s everywhereCustom metric (player)
CDN cache hit ratio85-95%> 95%Front Door analytics
WebSocket reconnect< 3s< 3s (with region failover)Custom metric
Upload throughput~50 Mbps~50 Mbps per regionLoad testing
Time to first byte (TTFB)< 100ms EU< 100ms everywhereLighthouse / WebPageTest

Lessons from Hanteo's Scale

Hanteo Global operates at 15M+ annual users, 1B+ page views, 200+ countries. Key architectural lessons:

  1. Data is the moat, not compute — Hanteo's 32-year chart data history is unreplicable. Sonnance should invest in data retention and enrichment, not just faster servers.
  2. Regional presence matters — Hanteo has physical offices in Chile, Mexico, Japan. For a digital platform, regional compute + storage achieves the same result without office overhead.
  3. Fan-scale traffic is spiky — K-POP comeback weeks, award voting, first-week sales campaigns generate massive traffic bursts. Auto-scaling is non-negotiable.
  4. Commerce requires local infrastructure — Hanteo's Whosfan Store operates in 32 countries with 99+ outlets. For Sonnance's digital commerce (beats, sessions, merch), payment processing must be localized (Stripe supports this, but tax compliance varies).
  5. Certification/trust requires consistency — Hanteo Chart's credibility comes from consistent, transparent data. Sonnance's data pipelines must be auditable and reproducible across regions.

Storage Tiering — Hot, Cool, Cold, Archive

Core question: Do we need hot/cold tiering if we have a CDN in front? Answer: Yes — CDN handles delivery tiering (cache hit = hot, origin fetch = warm). But origin storage tiering saves 60-90% on infrequently accessed files. CDN and storage tiers solve different problems.

Azure Blob Storage Access Tiers

TierAccess CostStorage CostFirst-Byte LatencyBest For
HotLow read/write~$0.018/GBMillisecondsActive projects, recent uploads, profile images
CoolHigher read, lower write~$0.010/GBMillisecondsProjects not touched in 30+ days, shared links, older versions
ColdHigher read~$0.0045/GBMillisecondsArchived projects, completed albums, old versions (90+ days)
ArchiveHighest read~$0.002/GBHours (rehydration)Legal holds, compliance copies, raw originals after processing (180+ days)

Lifecycle Management Policy

json
{
  "rules": [
    {
      "name": "audio-originals-lifecycle",
      "type": "Lifecycle",
      "definition": {
        "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["originals/"] },
        "actions": {
          "baseBlob": {
            "tierToCool": { "daysAfterLastAccessTimeGreaterThan": 30 },
            "tierToCold": { "daysAfterLastAccessTimeGreaterThan": 90 },
            "tierToArchive": { "daysAfterLastAccessTimeGreaterThan": 180 }
          }
        }
      }
    },
    {
      "name": "processed-audio-lifecycle",
      "type": "Lifecycle",
      "definition": {
        "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["processed/"] },
        "actions": {
          "baseBlob": {
            "tierToCool": { "daysAfterLastAccessTimeGreaterThan": 60 },
            "tierToCold": { "daysAfterLastAccessTimeGreaterThan": 120 }
          }
        }
      }
    },
    {
      "name": "image-variants-lifecycle",
      "type": "Lifecycle",
      "definition": {
        "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["images/"] },
        "actions": {
          "baseBlob": {
            "tierToCool": { "daysAfterLastAccessTimeGreaterThan": 90 }
          }
        }
      }
    }
  ]
}

How CDN + Storage Tiers Work Together

CDN + STORAGE TIER INTERACTION
═══════════════════════════════════════════════════════

  Request for track audio file:
  
  1. User requests track → hits Azure Front Door (CDN)
  2. Front Door checks edge cache:
     ├── CACHE HIT → serve from edge (< 10ms) ✅
     │   (CDN is effectively the "hot" layer for delivery)
     └── CACHE MISS → fetch from origin blob storage ⬇️
  
  3. Origin fetch depends on STORAGE TIER:
     ├── HOT tier → instant response (~20ms)
     ├── COOL tier → instant response (~20ms, higher per-read cost)
     ├── COLD tier → instant response (~20ms, even higher per-read cost)
     └── ARCHIVE tier → ❌ FAILS (must rehydrate first, takes hours)
  
  Key insight:
  → CDN cache handles the DELIVERY optimization (latency)
  → Storage tiers handle the COST optimization ($/GB/month)
  → Never archive files that users might play on demand
  → Archive is ONLY for raw originals kept for legal/compliance
  
  Smart pattern:
  → Processed audio (MP3/AAC) stays Hot or Cool (playable anytime)
  → Original WAV/FLAC moves to Cool → Cold → Archive over time
  → If user needs original → request triggers rehydration (show ETA)
  → CDN re-caches anything fetched from Cool/Cold automatically

Cost Savings Projection

Scenario100 GB stored1 TB stored10 TB stored
All Hot$1.80/mo$18.00/mo$180.00/mo
With tiering (60% Cool, 20% Cold, 10% Archive)$0.69/mo$6.90/mo$69.00/mo
Savings62%62%62%

At 10 TB (realistic for a growing audio platform with originals), tiering saves ~$110/mo. Not critical now, but compound savings matter at scale.


Originals vs. Processed: Audio & Image Storage Architecture

Current state: All files go into a single uploads container with flat key paths. No separation between originals and processed variants. Images already generate thumb, medium, and original WebP variants.

Container & Path Structure (Target)

STORAGE ACCOUNT: sonnancewebappstorage00
═══════════════════════════════════════════════════════

  Container: originals/          ← Untouched user uploads
  ├── audio/
  │   ├── {spaceId}/{projectId}/{trackId}/v{N}.wav
  │   ├── {spaceId}/{projectId}/{trackId}/v{N}.flac
  │   └── {spaceId}/{projectId}/{trackId}/v{N}.mp3
  ├── images/
  │   └── {spaceId}/{projectId}/cover.{ext}       ← Raw upload
  └── documents/
      └── {spaceId}/{projectId}/liner-notes.pdf

  Container: processed/          ← Optimized for delivery
  ├── audio/
  │   ├── {spaceId}/{projectId}/{trackId}/v{N}.mp3     ← 320kbps transcode
  │   ├── {spaceId}/{projectId}/{trackId}/v{N}.aac     ← 256kbps (Apple)
  │   ├── {spaceId}/{projectId}/{trackId}/v{N}.opus    ← 128kbps (mobile)
  │   └── {spaceId}/{projectId}/{trackId}/v{N}-preview.mp3  ← 30s preview
  ├── images/
  │   ├── {spaceId}/{projectId}/cover-thumb.webp   ← 150x150
  │   ├── {spaceId}/{projectId}/cover-medium.webp  ← 600px
  │   └── {spaceId}/{projectId}/cover-large.webp   ← 1200px
  └── waveforms/
      └── {spaceId}/{projectId}/{trackId}/v{N}.json  ← Pre-computed peaks

  Container: temp/               ← Upload staging, auto-delete 24h
  └── {uploadId}/{filename}      ← Chunked uploads land here first

Why Separate Originals from Processed

ConcernSingle Container (current)Separated Containers (target)
Lifecycle policiesCan't tier originals differently from processedOriginals auto-archive after 180d, processed stay Hot/Cool
Access patternsAll blobs same SAS permissionsOriginals: restricted access. Processed: CDN-friendly
Backup/DRMust backup everything identicallyOriginals: GZRS (max durability). Processed: LRS (re-creatable)
Cost~$0.018/GB for everythingOriginals on Cool/Cold save ~60%
DeletionAccidental delete loses bothOriginals have soft-delete + 30-day retention. Processed are re-generable
ComplianceHard to prove data lineageOriginals are immutable proof of upload (write-once, never modified)

Immutable Storage for Originals

For enterprise clients and legal compliance, enable immutable blob storage on the originals container:

bash
# Enable version-level immutability on the originals container
az storage container immutability-policy create \
  --account-name sonnancewebappstorage00 \
  --container-name originals \
  --period 365 \
  --allow-protected-append-writes-all true

This guarantees that once an audio original is uploaded, it cannot be modified or deleted for the retention period — critical for legal disputes, copyright claims, and master recording provenance.

Audio Processing Pipeline (Future)

AUDIO UPLOAD → PROCESSING PIPELINE
═══════════════════════════════════════════════════════

  1. User uploads track (WAV/FLAC/MP3)
     └── File lands in temp/ container (chunked if large)

  2. Upload complete → move to originals/audio/{path}
     └── Blob tier: Hot (initial access expected)

  3. Queue job: audio-processing
     ├── Transcode → MP3 320kbps  → processed/audio/{path}.mp3
     ├── Transcode → AAC 256kbps  → processed/audio/{path}.aac
     ├── Transcode → Opus 128kbps → processed/audio/{path}.opus
     ├── Generate preview (30s)   → processed/audio/{path}-preview.mp3
     ├── Generate waveform peaks   → processed/waveforms/{path}.json
     └── Extract metadata (BPM, key, duration) → DB update

  4. CDN serves from processed/ container
     └── Front Door origin: processed container only

  5. Original download (on demand)
     ├── Check tier: if Cool/Cold → serve directly
     ├── Check tier: if Archive → queue rehydration, notify user
     └── Generate short-lived SAS token (1h, read-only)

  Future (with BullMQ):
  └── Queue workers scale independently per region

Encryption — At Rest, In Transit, Key Management

Current State vs. Target

LayerCurrentTargetWhy
In transit✅ TLS 1.2+ enforced (Azure default)✅ Maintain + TLS 1.3 when availableAll traffic encrypted in flight
At rest (storage)✅ SSE with Microsoft-managed keys (default)CMK via Key VaultPlatform-managed keys are fine for most, but enterprise clients expect BYOK
At rest (database)✅ MongoDB Atlas encrypts at rest (default)Atlas BYOK with Azure Key VaultRequired for enterprise compliance (SOC 2, ISO 27001)
Application-level❌ NoneField-level encryption for PIIEncrypt email, phone, payment tokens before writing to DB
Secrets⚠️ Env vars in Container AppsAzure Key Vault with managed identitySecrets rotation, audit trail, RBAC scoping

Encryption Architecture

ENCRYPTION LAYERS
═══════════════════════════════════════════════════════

  ┌──────────────────────────────────────────────────┐
  │ Layer 4: APPLICATION-LEVEL ENCRYPTION            │
  │                                                  │
  │ MongoDB Client-Side Field Level Encryption (CSFLE)│
  │ → PII fields (email, phone) encrypted before     │
  │   leaving the application                        │
  │ → DB admins see ciphertext, not plaintext         │
  │ → Key stored in Azure Key Vault                   │
  │                                                  │
  │ When: Enterprise tier / SOC 2 compliance          │
  └──────────────────────────────────────────────────┘
  
  ┌──────────────────────────────────────────────────┐
  │ Layer 3: DATABASE ENCRYPTION AT REST              │
  │                                                  │
  │ MongoDB Atlas: AES-256 (automatic)               │
  │ → Default: Atlas-managed keys                     │
  │ → Target: BYOK via Azure Key Vault               │
  │   (Atlas → Azure Key Vault integration available) │
  └──────────────────────────────────────────────────┘
  
  ┌──────────────────────────────────────────────────┐
  │ Layer 2: STORAGE ENCRYPTION AT REST               │
  │                                                  │
  │ Azure Blob Storage: AES-256 SSE (automatic)      │
  │ → Default: Microsoft-managed keys (current) ✅    │
  │ → Target: Customer-managed keys (CMK) via KV     │
  │ → Scoped per container (originals = CMK,          │
  │   processed = Microsoft-managed is fine)          │
  └──────────────────────────────────────────────────┘
  
  ┌──────────────────────────────────────────────────┐
  │ Layer 1: NETWORK ENCRYPTION IN TRANSIT            │
  │                                                  │
  │ TLS 1.2+ everywhere (Azure enforced) ✅           │
  │ → Front Door → Container Apps: mTLS (optional)   │
  │ → Container Apps → MongoDB Atlas: TLS required    │
  │ → Container Apps → Blob Storage: HTTPS only       │
  │ → WebSocket: WSS (encrypted) only                │
  └──────────────────────────────────────────────────┘

Customer-Managed Keys (CMK) with Key Vault

bash
# 1. Create Key Vault (if not exists)
az keyvault create \
  --name wavic-kv \
  --resource-group Sonnance-WebApp \
  --location westeurope \
  --sku standard \
  --enable-purge-protection true \
  --enable-soft-delete true \
  --retention-days 90

# 2. Create encryption key
az keyvault key create \
  --vault-name wavic-kv \
  --name wavic-storage-cmk \
  --kty RSA \
  --size 2048

# 3. Assign managed identity to storage account
az storage account update \
  --name sonnancewebappstorage00 \
  --resource-group Sonnance-WebApp \
  --assign-identity

# 4. Grant storage account access to Key Vault key
STORAGE_IDENTITY=$(az storage account show \
  --name sonnancewebappstorage00 \
  --query "identity.principalId" -o tsv)

az keyvault set-policy \
  --name wavic-kv \
  --object-id $STORAGE_IDENTITY \
  --key-permissions get unwrapKey wrapKey

# 5. Configure CMK on storage account
az storage account update \
  --name sonnancewebappstorage00 \
  --resource-group Sonnance-WebApp \
  --encryption-key-vault https://wavic-kv.vault.azure.net \
  --encryption-key-name wavic-storage-cmk \
  --encryption-key-source Microsoft.Keyvault

Key Rotation Strategy

Key TypeRotation FrequencyMethodAutomation
Storage CMKEvery 90 daysAzure Key Vault auto-rotationBuilt-in policy
JWT signing secretEvery 90 daysKey Vault + Container App restartGitHub Actions workflow
MongoDB connection stringOn compromise onlyAtlas credential rotationManual (Atlas doesn't auto-rotate)
Stripe API keyAnnuallyStripe dashboard + Key Vault updateManual
SAS tokensGenerated per-request (short-lived)Already short-lived in codeAutomatic (1-hour tokens for downloads, 1-year for uploads)
OAuth client secretsAnnuallyGoogle/Apple console + Key VaultManual

Secrets Management — Azure Key Vault Integration

Current state: Secrets stored as Container Apps environment variables and GitHub Actions secrets. No centralized vault. No rotation. No audit log.

Target: Azure Key Vault as single source of truth. Container Apps read from Key Vault via managed identity. Zero secrets in env vars or code.

Migration Path: Env Vars → Key Vault

SECRETS MIGRATION
═══════════════════════════════════════════════════════

  CURRENT (insecure but functional):
  ┌──────────────────────────────────┐
  │ Container Apps Environment       │
  │ ├── JWT_SECRET=xxxxx             │
  │ ├── MONGODB_URI=mongodb+srv://...│
  │ ├── STRIPE_API_KEY=sk_live_...   │
  │ ├── AZURE_STORAGE_ACCOUNT_KEY=...│
  │ └── GOOGLE_CLIENT_SECRET=...     │
  └──────────────────────────────────┘
  (visible to anyone with Azure portal access,
   no rotation tracking, no audit)

  TARGET:
  ┌──────────────┐          ┌──────────────────┐
  │ Container    │─────────▶│ Azure Key Vault   │
  │ Apps         │ managed  │ ├── jwt-secret    │
  │ (system      │ identity │ ├── mongodb-uri   │
  │  managed ID) │          │ ├── stripe-key    │
  │              │          │ ├── google-secret  │
  │              │          │ └── (NO storage   │
  │              │          │      key — use    │
  │              │          │      managed ID)  │
  └──────────────┘          └──────────────────┘
  (secrets never exposed in env vars,
   rotation tracked, access audited,
   RBAC scoped per secret)

Implementation: NestJS Key Vault Client

typescript
// key-vault.service.ts — future module
import { SecretClient } from '@azure/keyvault-secrets';
import { DefaultAzureCredential } from '@azure/identity';

@Injectable()
export class KeyVaultService {
  private client: SecretClient;

  constructor() {
    const vaultUrl = process.env.KEY_VAULT_URL; // only URL in env vars
    this.client = new SecretClient(vaultUrl, new DefaultAzureCredential());
  }

  async getSecret(name: string): Promise<string> {
    const secret = await this.client.getSecret(name);
    return secret.value;
  }
}

Managed Identity Replaces Storage Keys

STORAGE KEY ELIMINATION
═══════════════════════════════════════════════════════

  CURRENT:
  → AZURE_STORAGE_ACCOUNT_KEY in env vars
  → SharedKeyCredential in blob-storage.service.ts
  → Key rotated? Never. Key exposed? To all devs.

  TARGET:
  → Container Apps system-assigned managed identity
  → DefaultAzureCredential (auto-resolves in Azure)
  → Storage account grants "Storage Blob Data Contributor"
    role to the managed identity
  → In local dev: falls back to Azure CLI login or env vars
  → Zero keys to manage, rotate, or leak
bash
# Enable managed identity on Container Apps
az containerapp identity assign \
  --name ctrl-audio-backend \
  --resource-group Sonnance-WebApp \
  --system-assigned

# Grant blob access to the managed identity
IDENTITY_PRINCIPAL=$(az containerapp identity show \
  --name ctrl-audio-backend \
  --resource-group Sonnance-WebApp \
  --query "principalId" -o tsv)

az role assignment create \
  --assignee $IDENTITY_PRINCIPAL \
  --role "Storage Blob Data Contributor" \
  --scope /subscriptions/{sub-id}/resourceGroups/Sonnance-WebApp/providers/Microsoft.Storage/storageAccounts/sonnancewebappstorage00

Secret Categories & Their Vault Path

SecretKey Vault NameRotationAccess Scope
JWT_SECRETwavic-jwt-secret90 daysBackend only
MONGODB_URIwavic-mongodb-uriOn compromiseBackend only
STRIPE_API_KEYwavic-stripe-keyAnnualBackend (subscription module)
STRIPE_WEBHOOK_SECRETwavic-stripe-webhookAnnualBackend (webhook endpoint)
GOOGLE_CLIENT_SECRETwavic-google-oauthAnnualBackend (auth module)
APPLE_CLIENT_SECRETwavic-apple-oauthAnnualBackend (auth module)
SENTRY_DSNwavic-sentry-dsnNeverBackend + Frontend
Azure StorageEliminated — use managed identityN/AN/A

Network Security, Private Endpoints & VPN

Current state: All services communicate over public internet. Container Apps ingress is open. MongoDB Atlas uses IP allowlist. No VNet.

Target: Zero-trust network where Backend ↔ Storage ↔ Database communication flows over private network. Public access only through Front Door.

Network Architecture (Phased)

PHASE 1: CURRENT (Public Endpoints)
═══════════════════════════════════════════════════════

  Internet ──── Container Apps (public) ──── MongoDB Atlas (IP allowlist)
      │                  │
      │                  └──── Azure Blob (public with SAS)

      └──── Front Door (CDN) ──── Blob Storage (public origin)


PHASE 2: PRIVATE ENDPOINTS (Pre-Enterprise)
═══════════════════════════════════════════════════════

  Internet ──── Front Door ──── WAF ────┐
                                        │ (Private Link)
                   ┌────────────────────┤
                   │                    │
              ┌────▼─────────────┐      │
              │   VNet           │      │
              │   (10.0.0.0/16)  │      │
              │                  │      │
              │ ┌──────────────┐ │      │
              │ │ Subnet:      │ │      │
              │ │ apps         │ │      │
              │ │ (10.0.1.0/24)│ │      │
              │ │              │ │      │
              │ │ Container    │ │      │
              │ │ Apps Env     │ │      │
              │ └──────┬───────┘ │      │
              │        │         │      │
              │ ┌──────▼───────┐ │ ┌────▼──────────┐
              │ │ Subnet:      │ │ │ Subnet:       │
              │ │ storage-pe   │ │ │ frontdoor-pe  │
              │ │ (10.0.2.0/24)│ │ │ (10.0.4.0/24) │
              │ │              │ │ │               │
              │ │ Blob PE ●    │ │ │ FD PE ●       │
              │ │ Redis PE ●   │ │ └───────────────┘
              │ │ KV PE ●      │ │
              │ └──────────────┘ │
              │                  │
              │ ┌──────────────┐ │
              │ │ Subnet:      │ │
              │ │ db-pe        │ │
              │ │ (10.0.3.0/24)│ │
              │ │              │ │
              │ │ MongoDB      │ │
              │ │ Atlas PE ●   │ │  ← Atlas supports Azure Private Link
              │ └──────────────┘ │
              └──────────────────┘

  ● = Private Endpoint (traffic never touches public internet)


PHASE 3: ENTERPRISE VPN (For Enterprise Clients)
═══════════════════════════════════════════════════════

  Enterprise Client Network ──── VPN Gateway / ExpressRoute
      │                                  │
      │          ┌───────────────────────┤
      │          │                       │
      │    ┌─────▼──────────────┐        │
      │    │  Wavic VNet         │       │
      │    │  (same as Phase 2)  │       │
      │    │                     │       │
      │    │  + VPN Gateway      │       │
      │    │    subnet           │       │
      │    │  + Point-to-Site    │       │
      │    │    OR Site-to-Site  │       │
      │    └─────────────────────┘       │
      │                                  │
      └── Enterprise can access Wavic    │
          backend over private network   │
          (no public internet exposure)  │

Private Endpoints — What Connects Where

ServicePrivate EndpointSubnetPublic Access After PE
Azure Blob Storagesonnance-storage-pestorage-peDisabled (Front Door uses Private Link)
Azure Key Vaultwavic-kv-pestorage-peDisabled
Azure Cache for Rediswavic-redis-pestorage-peDisabled
MongoDB AtlasAtlas Private Endpointdb-peDisabled (Atlas VPC Peering or Private Link)
Azure Front DoorFront Door Private Link originfrontdoor-pePublic (it IS the public entry point)

VPN for Enterprise Clients

Some enterprise clients (labels, distributors) may require VPN connectivity as a security condition:

VPN OptionWhat It DoesCostUse Case
Point-to-Site (P2S)Individual users connect via client VPN~$140/mo (VpnGw1)Enterprise users accessing admin tools, sensitive data
Site-to-Site (S2S)Enterprise network connects to Wavic VNet~$140/mo (VpnGw1)Label integrates their internal systems with Wavic API
ExpressRouteDedicated private circuit~$300+/moLarge enterprise with strict compliance (rare for SaaS)
Azure BastionJump host for admin access~$140/moSysAdmin access to backend without VPN

Recommended path: Don't deploy VPN proactively. It's an enterprise add-on:

  1. Now → IP allowlisting is sufficient
  2. First enterprise client → Deploy VPN Gateway (P2S) in the VNet
  3. Large label integration → Site-to-Site VPN for their API integration
  4. Compliance-heavy client (government/military) → ExpressRoute

Network Security Groups (NSGs)

bash
# NSG for Container Apps subnet — allow only Front Door + internal traffic
az network nsg create --name wavic-apps-nsg --resource-group Sonnance-WebApp

az network nsg rule create \
  --nsg-name wavic-apps-nsg \
  --name AllowFrontDoor \
  --priority 100 \
  --source-address-prefixes AzureFrontDoor.Backend \
  --destination-port-ranges 443 \
  --access Allow \
  --direction Inbound

az network nsg rule create \
  --nsg-name wavic-apps-nsg \
  --name DenyAllOtherInbound \
  --priority 4096 \
  --source-address-prefixes "*" \
  --destination-port-ranges "*" \
  --access Deny \
  --direction Inbound

Burst & Spike Resilience — The 300K-in-40-Minutes Problem

Context: Hanteo experienced outages when 300K+ users hit the platform within a 40-minute window (K-POP comeback events, award voting). Sonnance must architect for similar spikes — album drops, playlist launches, live sessions, awards.

Traffic Spike Scenarios for Sonnance

EventExpected SpikeDurationPattern
Album drop / release day10-100x normal1-4 hoursThundering herd: all fans hit "play" simultaneously
Live collaboration session5-20x normal WebSocket1-2 hoursSustained high WebSocket connections
Award voting (future)50-500x normal writes30-60 minWrite-heavy, requires idempotency
Viral social media moment10-1000x normal reads2-24 hoursRead-heavy, CDN absorbs most
Enterprise onboardingBulk upload (TB of audio)Hours-DaysStorage I/O, not compute

Architecture for Burst Resilience

BURST RESILIENCE ARCHITECTURE
═══════════════════════════════════════════════════════

  ┌─────────────────────────────────────────────────┐
  │ LAYER 1: EDGE ABSORPTION (Front Door + CDN)     │
  │                                                 │
  │ → 95%+ of reads served from edge cache           │
  │ → WAF rate limits abusive clients                │
  │ → DDoS protection (Azure DDoS Standard)          │
  │ → Audio/images are immutable — infinite cache    │
  │                                                 │
  │ Absorbs: Viral moments, play-count spikes        │
  │ Capacity: Essentially unlimited (Azure CDN edge) │
  └─────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────┐
  │ LAYER 2: COMPUTE AUTO-SCALING (Container Apps)   │
  │                                                 │
  │ → KEDA-based scaling: scale on HTTP concurrency, │
  │   queue depth, or custom metrics                 │
  │ → Pre-warm instances before known events          │
  │ → Scale 0 → 50 replicas in ~60s                  │
  │ → Independent scaling per region                  │
  │                                                 │
  │ Absorbs: API request spikes, WebSocket surges    │
  │ Capacity: Up to 300 replicas per Container App   │
  └─────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────┐
  │ LAYER 3: QUEUE-BASED LOAD LEVELING              │
  │                                                 │
  │ → Writes go to queue (Azure Service Bus or       │
  │   BullMQ/Redis) instead of direct DB writes      │
  │ → Consumers process at controlled rate            │
  │ → Prevents DB overwhelm during write spikes      │
  │ → Idempotent consumers — safe to retry            │
  │                                                 │
  │ Absorbs: Comment floods, vote surges, bulk ops   │
  │ Capacity: Queue depth = unlimited buffer          │
  └─────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────┐
  │ LAYER 4: DATABASE PROTECTION                     │
  │                                                 │
  │ → Read replicas absorb read traffic               │
  │ → Connection pooling (MongoDB driver maxPoolSize) │
  │ → Circuit breaker: shed load if DB is saturated   │
  │ → Stale reads acceptable for non-critical data    │
  │   (e.g., play counts can lag 30s)                │
  │                                                 │
  │ Protects: MongoDB Atlas from connection storms    │
  │ Pattern: CQRS-lite (separate read/write paths)   │
  └─────────────────────────────────────────────────┘

Container Apps Scaling Configuration for Spikes

bash
# Production spike-ready scaling rules
az containerapp update \
  --name ctrl-audio-backend \
  --resource-group Sonnance-WebApp \
  --min-replicas 2 \
  --max-replicas 50 \
  --scale-rule-name http-burst \
  --scale-rule-type http \
  --scale-rule-http-concurrency 50

# Pre-warm before known events (album drop, awards)
az containerapp update \
  --name ctrl-audio-backend \
  --resource-group Sonnance-WebApp \
  --min-replicas 10  # Pre-warm 10 instances 1 hour before event

Circuit Breaker Pattern

typescript
// circuit-breaker.service.ts — protect downstream services
@Injectable()
export class CircuitBreakerService {
  private failures = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  private lastFailure: number = 0;

  private readonly THRESHOLD = 5;       // Open after 5 failures
  private readonly TIMEOUT = 30_000;    // Try again after 30s
  
  async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.TIMEOUT) {
        this.state = 'half-open';
      } else if (fallback) {
        return fallback();
      } else {
        throw new ServiceUnavailableException('Service temporarily unavailable');
      }
    }

    try {
      const result = await fn();
      this.reset();
      return result;
    } catch (error) {
      this.recordFailure();
      throw error;
    }
  }

  private recordFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.THRESHOLD) this.state = 'open';
  }

  private reset() {
    this.failures = 0;
    this.state = 'closed';
  }
}

Queue-Based Write Leveling

WRITE SPIKE PROTECTION
═══════════════════════════════════════════════════════

  Without queue (current — risky under spike):
  
  User writes → API → MongoDB (direct)
  ├── 300K writes in 40min = 125 writes/sec
  ├── MongoDB M10 max connections: 1,500
  ├── If each write takes 10ms → 100 writes/sec sustainable
  └── RESULT: Connection pool exhaustion, timeouts, errors

  With queue (target):
  
  User writes → API → Queue (instant ack) → Consumer → MongoDB
  ├── API returns 202 Accepted immediately
  ├── Queue buffers unlimited writes
  ├── Consumer processes at 100 writes/sec (safe DB rate)
  ├── User sees result via WebSocket push when processed
  └── RESULT: Graceful degradation, zero dropped writes

  Use for: comments, reactions, play-count updates, vote tallying
  Keep synchronous: auth, file uploads, critical reads

Hanteo Failure Analysis — Lessons Applied

Hanteo IssueRoot CauseSonnance Mitigation
App crash at 300K concurrentLikely single-region, no auto-scalingMulti-region + Container Apps 0→300 auto-scale
Voting system overwhelmSynchronous writes to DBQueue-based write leveling (async votes)
Data inconsistency during spikesRace conditions on countersAtomic MongoDB operations ($inc) + idempotent queue consumers
Long recovery timeNo circuit breakers, cascading failuresCircuit breaker + graceful degradation (stale reads)
Repeat occurrencesNo pre-warming for predictable eventsEvent-aware pre-scaling (min-replicas bump before known events)

Monitoring, Observability & SysAdmin Automation Platform

The goal: A platform ops team of 1-2 people should be able to manage the entire global infrastructure. If it needs manual intervention, it should be automated. If an alert fires, it should include the runbook link.

Observability Stack

LayerToolWhat It CapturesCost
APM / TracesAzure Application Insights (or OpenTelemetry → Grafana)Request traces, dependency maps, exceptions, performanceFree tier → ~$2.30/GB ingested
LogsAzure Log Analytics WorkspaceContainer logs, audit logs, Key Vault accessFree 5GB/mo → ~$2.76/GB
MetricsAzure Monitor + Prometheus (via Container Apps)CPU, memory, HTTP 5xx rate, latency percentilesIncluded
UptimeAzure Availability Tests (or Checkly, BetterUptime)Global endpoint checks every 1-5 min~$1/test/mo (Azure native)
Error trackingSentry (already deployed)Frontend + backend exceptions with stack tracesFree tier (5K events/mo)
Real User MonitoringVercel Analytics or custom (Application Insights JS SDK)Page load time, Web Vitals, client errorsFree → $10/mo
CostAzure Cost Management + budgetsSpend tracking, forecasting, anomaly alertsFree

Alert Strategy — Actionable, Not Noisy

ALERT SEVERITY FRAMEWORK
═══════════════════════════════════════════════════════

  P0 — CRITICAL (page on-call immediately)
  ├── Health endpoint returns 5xx for > 2 minutes
  ├── Database connection failures > 10 in 1 minute  
  ├── Storage account unavailable
  ├── SSL certificate expiry < 7 days
  └── Action: PagerDuty/Slack alert → on-call responds in 15min

  P1 — HIGH (notify within 1 hour)
  ├── API p99 latency > 2s for > 5 minutes
  ├── Error rate > 5% for > 5 minutes
  ├── Container restarts > 3 in 10 minutes
  ├── Queue depth > 10,000 (processing falling behind)
  └── Action: Slack alert → investigate within 1 hour

  P2 — MEDIUM (review next business day)
  ├── CDN cache hit ratio < 80%
  ├── Database slow queries > 500ms (p95)
  ├── Memory usage > 80% sustained
  ├── Cost anomaly (> 30% spike vs. forecast)
  └── Action: Ticket created → review in 24 hours

  P3 — LOW (weekly review)
  ├── Dependency vulnerability found (npm audit)
  ├── Container image age > 30 days
  ├── Unused resources detected
  └── Action: Added to weekly ops review

SysAdmin Automation — Build vs. Buy

OperationFrequencyManual EffortAutomation ApproachBuild/Buy
Deploy to all regionsOn merge to main5 minGitHub Actions matrix strategyBuild (already have CI/CD)
SSL certificate rotationEvery 90 days15 minAzure managed certs (auto)Buy (Azure built-in)
Secret rotationEvery 90 days30 minKey Vault auto-rotation + pipelineBuild (Key Vault policy + GitHub Action)
Database backupsDaily0 minMongoDB Atlas automated backupsBuy (Atlas built-in)
Cost auditWeekly30 minAzure Cost Management + budget alertsBuy (Azure built-in)
Vulnerability scanningOn PR + weekly10 minGitHub Dependabot + Docker ScoutBuy (GitHub built-in)
Log retention cleanupMonthly0 minLog Analytics retention policyBuy (Azure built-in)
Scaling decisionsOn demand15 minKEDA auto-scaling (Container Apps)Buy (built into platform)
Incident runbooksOn alertVariesAutomated runbooks (Azure Automation or n8n)Build
Health check monitoringContinuous0 minAzure Availability Tests / ChecklyBuy
Orphan resource cleanupMonthly20 minAzure Resource Graph query + alertBuild (script)
Performance regressionOn deploy0 minLoad test in CI pipeline (k6 or Artillery)Build (CI step)

Automated Runbooks — Self-Healing Operations

SELF-HEALING AUTOMATION
═══════════════════════════════════════════════════════

  Trigger: P0 alert "Backend health check failing"
  ┌──────────────────────────────────────────────┐
  │ Automated Response (within 60 seconds):       │
  │                                              │
  │ 1. Check: Is it a single replica or all?     │
  │    ├── Single → Container Apps auto-restarts │
  │    └── All → escalate to step 2              │
  │                                              │
  │ 2. Check: Is database reachable?             │
  │    ├── No → switch to read-only mode         │
  │    │   (serve cached data, queue writes)     │
  │    └── Yes → escalate to step 3              │
  │                                              │
  │ 3. Check: Is it a deploy regression?         │
  │    ├── Yes → auto-rollback to previous image │
  │    └── No → page on-call with full context   │
  │                                              │
  │ 4. Notify: Slack #ops with:                  │
  │    ├── What failed                           │
  │    ├── What was auto-remediated              │
  │    ├── Current status                        │
  │    └── Runbook link for manual follow-up     │
  └──────────────────────────────────────────────┘

  Tech options for automation runner:
  ├── Azure Automation Runbooks (PowerShell/Python)
  │   → Best for Azure-native operations
  │   → Free 500 min/mo

  ├── n8n (self-hosted or cloud)
  │   → Already considered for integrations (doc 18)
  │   → Visual workflow builder, good for ops too
  │   → Can trigger Azure CLI, call APIs, send Slack

  ├── GitHub Actions (workflow_dispatch)
  │   → Already have CI/CD here
  │   → Can trigger runbooks on webhook from alerts
  │   → Free 2,000 min/mo

  └── Custom NestJS cron jobs
      → Already have @nestjs/schedule
      → Good for app-level health (orphan cleanup, etc.)
      → NOT for infra-level ops (can't restart itself)

Infrastructure Dashboard

One dashboard to rule them all — a single pane that shows global system health:

PLATFORM OPS DASHBOARD (Azure Portal or Grafana)
═══════════════════════════════════════════════════════

  ┌─────────────────────────────────────────────────┐
  │ GLOBAL HEALTH                    Status: ✅ OK   │
  ├──────────┬──────────┬───────────┬───────────────┤
  │ EU       │ Americas │ APAC      │ Global CDN    │
  │ ✅ OK    │ ✅ OK    │ 🔶 Scaling │ ✅ 97% hit   │
  │ 3 pods   │ 2 pods   │ 8 pods    │ p50: 12ms    │
  │ p50: 22ms│ p50: 18ms│ p50: 45ms │              │
  ├──────────┴──────────┴───────────┴───────────────┤
  │ DATABASE                                         │
  │ Primary: EU   │ Replicas: US ✅, KR ✅           │
  │ Connections: 142/1500  │  Ops/sec: 850           │
  │ Replication lag: 12ms (US), 45ms (KR)           │
  ├──────────────────────────────────────────────────┤
  │ STORAGE                                          │
  │ Total: 2.4 TB  │ Hot: 800 GB  │ Cool: 1.2 TB   │
  │ Cold: 350 GB   │ Archive: 50 GB                 │
  │ Ingress today: 12 GB  │ Egress: 145 GB          │
  ├──────────────────────────────────────────────────┤
  │ COSTS (MTD)                   Budget: $300/mo    │
  │ Compute: $42   │ DB: $171  │ Storage: $8         │
  │ CDN: $18       │ Redis: $52 │ Other: $12         │
  │ Total: $303    │ Forecast: $310 ⚠️               │
  ├──────────────────────────────────────────────────┤
  │ RECENT ALERTS                                    │
  │ 🔶 14:32 APAC scale-up triggered (8 replicas)   │
  │ ✅ 14:33 APAC scale-up completed                │
  │ ✅ 09:15 Daily backup completed (all regions)   │
  └──────────────────────────────────────────────────┘

Cost Governance Automation

bash
# Azure budget alert — notify when spend approaches limit
az consumption budget create \
  --budget-name wavic-monthly \
  --amount 400 \
  --time-grain Monthly \
  --start-date 2026-04-01 \
  --end-date 2027-04-01 \
  --resource-group Sonnance-WebApp \
  --notifications '[
    {"enabled": true, "operator": "GreaterThanOrEqualTo",
     "threshold": 80, "contactEmails": ["ops@wavic.io"]},
    {"enabled": true, "operator": "GreaterThanOrEqualTo",
     "threshold": 100, "contactEmails": ["ops@wavic.io", "ceo@wavic.io"]}
  ]'

Load Testing in CI (Pre-Deploy Validation)

Before deploying to production, automated load tests catch performance regressions:

yaml
# .github/workflows/load-test.yml (runs on staging before prod deploy)
load-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Install k6
      run: |
        sudo gpg -k
        sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
        echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
        sudo apt-get update && sudo apt-get install k6
    - name: Run load test
      run: k6 run tests/load/spike-test.js
      env:
        K6_TARGET_URL: ${{ vars.STAGING_URL }}
    - name: Check thresholds
      if: failure()
      run: echo "Load test failed — blocking production deploy"

Data Replication Strategy

Distinct from multi-region architecture (which focuses on compute), this section covers data replication — ensuring data durability, disaster recovery, and cross-region consistency.

Replication Matrix

Data TypeSize ProfileReplication StrategyRPORTO
User data (DB)Small (~1 KB/user)MongoDB Atlas replicas + point-in-time recovery< 1 min< 15 min
Project metadata (DB)Small (~5 KB/project)Same as user data< 1 min< 15 min
Audio originals (Blob)Large (10-100 MB/file)GRS or GZRS (paired region failover)< 15 min< 1 hour
Processed audio (Blob)Medium (5-30 MB/file)LRS (re-generable from originals)N/A (re-create)< 4 hours
Images (Blob)Small (50-500 KB/file)GRS (auto-replicated)< 15 min< 1 hour
Waveform data (Blob)Tiny (10-50 KB/file)LRS (re-generable)N/A (re-create)< 1 hour
Secrets (Key Vault)TinyAzure Key Vault auto-replicates within region pair< 1 min< 5 min
Redis cacheVolatileNo replication needed (cache is rebuildable)N/A< 1 min (cold start)

RPO / RTO vs. Cost Tradeoff

REPLICATION COST → DURABILITY SPECTRUM
═══════════════════════════════════════════════════════

  ◀─── Cheaper                           More Durable ───▶

  LRS           ZRS           GRS           GZRS          RA-GZRS
  ($0.018/GB)   ($0.023/GB)   ($0.036/GB)   ($0.046/GB)   ($0.061/GB)
  
  3 copies      3 copies      6 copies      6 copies      6 copies
  1 datacenter  3 zones       2 regions     3 zones +     3 zones +
                                            2 regions     2 regions +
                                                          read access
  
  Use for:      Use for:      Use for:      Use for:      Use for:
  Processed     Container     Audio         Audio         (overkill for
  audio,        Apps env,     originals,    originals     most cases)
  temp files    non-critical  user images   (max
                                            durability)

  RECOMMENDATION:
  → originals/ container: GZRS (maximum durability for irreplaceable files)
  → processed/ container: LRS (re-generable, save 60% cost)
  → temp/ container: LRS (auto-deleted after 24h)

Backup & Disaster Recovery

ComponentBackup MethodFrequencyRetentionRecovery
MongoDB AtlasAutomated snapshots + continuous backupContinuous (oplog)30 daysPoint-in-time restore to any second
Blob originalsGZRS auto-replication + soft deleteContinuous30-day soft delete + 365-day retentionRestore from paired region or soft-delete
Blob processedNot backed up (re-generate from originals)N/AN/ARe-run processing pipeline
Key VaultAzure-managed replicationContinuous90-day purge protectionAuto-recoverable
Container AppsInfrastructure as Code (no state to backup)On commitGit historyRe-deploy from IaC
DNS / Front DoorAzure-managed (global anyway)N/AN/AAuto-recoverable

Infrastructure Maturity Roadmap — Summary

Bringing together all sections into a phased execution plan:

PhaseWhenFocusKey ActionsMonthly Cost Δ
0 — CurrentNowShip product, iterate fastSingle region, public endpoints, env var secrets, all-Hot storage~$11/mo
1 — HardenPre-launchSecurity & reliability baselineKey Vault for secrets, managed identity, lifecycle policies on storage, Application Insights, structured alerts+$20-30/mo
2 — PrivatePost-launchLock down network, encrypt properlyVNet + private endpoints, CMK encryption, WAF on Front Door, separated containers (originals/processed/temp)+$40-80/mo
3 — Multi-Region10K+ usersGlobal latency & resilienceSecond region (East US 2), MongoDB replicas, multi-origin Front Door, queue-based writes+$150-200/mo
4 — EnterpriseFirst enterprise clientCompliance & isolationVPN Gateway, BYOK encryption, audit logging, SOC 2 prep, SLA commitments+$100-200/mo
5 — Global50K+ usersFull multi-continentThird+ regions, Global Clusters (zone sharding), auto-scaling per region, fully automated runbooks+$100-300/mo

Last Updated: March 2026

Ctrl-Audio Platform Documentation