Scalability Guide
Enterprise scalability patterns for global deployment
Current Stack (February 2026)
| Layer | Technology | Version |
|---|---|---|
| Frontend | Next.js (Azure Static Web Apps) | 16.1.6 |
| Backend | NestJS (Azure Container Apps) | 11.x |
| Database | MongoDB Atlas | 7.x |
| Storage | Azure Blob Storage | - |
| CDN | Azure Front Door (deployed) | - |
| CI/CD | GitHub 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:
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 100Scaling Triggers
| Metric | Recommendation |
|---|---|
| HTTP Requests | Scale at 100 concurrent connections |
| Memory | Scale at 70% memory utilization |
| Queue Depth | For background jobs (future) |
Database Optimization
MongoDB Indexes
Ensure indexes exist for common queries:
// 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
// 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:
cd infrastructure
chmod +x setup-cdn.sh
./setup-cdn.shCDN Benefits
| Metric | Without CDN | With CDN |
|---|---|---|
| Latency (EU) | 50-100ms | 10-20ms |
| Latency (US) | 150-200ms | 20-30ms |
| Bandwidth costs | $0.087/GB | $0.081/GB |
| Cache hit ratio | 0% | 85-95% |
CDN URL Integration
// 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:
// Cursor-based (recommended)
GET /tracks?cursor=507f1f77bcf86cd799439011&limit=20
// Response
{
items: [...],
nextCursor: "507f1f77bcf86cd799439012",
hasMore: true
}Response Compression
Enable gzip compression:
import * as compression from 'compression';
app.use(compression());Efficient Endpoints for Mobile
// ✅ 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=xxxImage Optimization for Mobile
Return appropriate image variants:
// 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
// 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:
@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
// 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):
import { BullModule } from '@nestjs/bull';
@Module({
imports: [
BullModule.registerQueue({
name: 'audio-processing',
}),
],
})Monitoring & Observability
Azure Application Insights
import * as appInsights from 'applicationinsights';
appInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY)
.setAutoCollectRequests(true)
.setAutoCollectDependencies(true)
.start();Health Checks
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
| Phase | Feature | Effort | Impact |
|---|---|---|---|
| 1 | Azure CDN | 1 hour | High |
| 2 | MongoDB indexes | 2 hours | High |
| 3 | Response compression | 30 min | Medium |
| 4 | Application Insights | 2 hours | Medium |
| 5 | Redis caching | 1 day | High |
| 6 | BullMQ for jobs | 2 days | Medium |
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)
| Aspect | Current | Global Target | Why |
|---|---|---|---|
| Regions | 1 (West Europe) | 3 (EU + Americas + APAC) | Cover 90%+ of global users within 100ms |
| Auto-scaling | 1-10 replicas | 1-10 per region | Independent scaling per traffic pattern |
| Deployment | Single | Blue-green per region | Zero-downtime deploys, regional rollback |
| Admin effort | Low | Low (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
| Approach | How It Works | Latency | Cost | Admin Effort | Best For |
|---|---|---|---|---|---|
| Single region (current) | One M0 cluster in AWS EU | 10ms EU, 300ms APAC | Free (M0) | None | MVP, < 1,000 users |
| Multi-region read replicas | Primary in EU, secondaries in US + APAC | < 50ms reads everywhere | M10+ (~$57/mo base + replica cost) | Low | 1K-100K users |
| Global Clusters (Zone Sharding) | Data pinned to regions by user location, reads always local | < 20ms reads, < 50ms writes | M30+ (~$500/mo+) | Medium | 100K+ users, data sovereignty |
| Atlas Edge Server | MongoDB Sync to edge devices/regions | < 10ms reads | Premium pricing | Medium-High | Mobile-first, offline-first |
Recommended path:
- Now → Stay on M0 (free), single region. Write reads and writes to EU.
- 1K-10K users → Upgrade to M10, add read replicas in US East + Korea Central.
- 10K-100K users → Atlas Global Cluster with zone sharding. Users' data lives in their closest region.
- 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):
// 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
| Strategy | How It Works | Latency | Cost | Admin |
|---|---|---|---|---|
| GRS (Geo-Redundant) | Automatic replication to paired region | Same origin latency, failover available | 2x LRS cost (~$0.036/GB) | None |
| GZRS | GRS + zone redundancy | Best durability | ~$0.046/GB | None |
| Multi-account + Front Door | Separate storage accounts per region, CDN serves nearest | < 20ms (cached) | Per-account cost + Front Door | Low |
| Azure Blob + Akamai/Cloudflare | Single origin, CDN edge serves globally | < 30ms (cached) | CDN fees | Low |
Recommended path for audio files:
- Now → Single storage + Front Door (deployed). CDN caches images/audio at edge.
- 10K+ users → Add storage accounts in US + APAC. Upload goes to nearest region. Front Door routes requests to nearest origin.
- 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
| Challenge | Solution | Technology |
|---|---|---|
| WebSocket connections must be sticky | Azure Container Apps supports sticky sessions via ARR Affinity | Built-in |
| Cross-region pub/sub | Redis pub/sub adapter for Socket.io — messages flow between regions | Azure Cache for Redis with geo-replication |
| Presence across regions | Shared Redis stores online status, any region can read it | Redis geo-replicated |
| Connection re-routing on failover | Front Door health probes detect region down → re-route connections | Azure 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 only5. CDN — Azure Front Door (expand configuration)
Already deployed (sonnance-cdn). For global scale:
| Improvement | What | When |
|---|---|---|
| Add origins per region | Storage accounts in US + APAC as additional origins | 10K+ users |
| Custom rules | Route audio vs. image vs. API traffic differently | Now (optimize cache TTLs) |
| WAF (Web Application Firewall) | DDoS protection, bot mitigation, geo-filtering | Before public launch |
| Private Link origins | Connect Front Door to storage via private network | Enterprise tier |
| Audio-specific caching | Longer TTL for audio (immutable once uploaded), shorter for profile images | Now |
Data Sovereignty & Compliance
Music platforms operate globally but data laws are local. Plan for this before it becomes a blocker.
| Region | Key Regulation | What It Means for Sonnance | Required By |
|---|---|---|---|
| EU | GDPR | User data stays in EU unless user consents. Right to delete, portability, DPO. | Now (existing users) |
| South Korea | PIPA (Personal Information Protection Act) | Stricter than GDPR on cross-border transfers. Local storage preferred. Consent for international transfer. | Before K-POP market push |
| Brazil | LGPD | GDPR-equivalent. Data processing consent. Local DPO representative. | Before LATAM expansion |
| US (California) | CCPA/CPRA | Consumer data rights, opt-out of sale, retention limits. No federal law yet. | Before US growth |
| China | PIPL + CSL | Data localization mandatory. Cross-border transfer requires security assessment. | Only if entering Chinese market |
| Japan | APPI | Consent for cross-border. Adequacy framework with EU. | Before Japan expansion |
| India | DPDP 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.
| Priority | Region | Azure Region | Why | Latency Gain | Estimated Users |
|---|---|---|---|---|---|
| P0 (current) | Europe | West Europe (Netherlands) | Home base, EU artists/labels | Baseline | 1K-10K |
| P1 | Americas | East US 2 (Virginia) | Largest music market, Latin America bridge | EU→US: 300ms→20ms | 10K-50K |
| P2 | Asia-Pacific | Korea Central (Seoul) | K-POP is $10B+ industry, Hanteo validates demand | EU→KR: 350ms→20ms | 5K-30K |
| P3 | Latin America | Brazil South (São Paulo) | 350M+ Spanish/Portuguese speakers, massive music culture | EU→BR: 250ms→20ms | 5K-20K |
| P4 | Japan/SEA | Japan East (Tokyo) | J-POP market, SEA growing | EU→JP: 300ms→20ms | 3K-15K |
| P5 | Middle East/Africa | UAE North (Dubai) | Emerging music tech market | EU→UAE: 150ms→20ms | 1K-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 requiresLow Admin Effort: Infrastructure as Code
The entire multi-region setup must be reproducible with one command. Zero manual configuration.
| Technology | Purpose | Why |
|---|---|---|
| Bicep / Terraform | Define all Azure resources as code | One template → deploy to any region |
| GitHub Actions | CI/CD per region | Push to main → deploy to all active regions |
| Azure Front Door | Global load balancing + failover | Automatic health checks, traffic routing |
| MongoDB Atlas API | Database region management | Terraform provider for Atlas cluster configuration |
| Helm charts (future, if AKS) | Container orchestration templates | Only if Container Apps limits are reached |
# 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 backendCost Modeling for Multi-Region
| Component | Single 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
| Metric | Single Region | Multi-Region Target | How to Measure |
|---|---|---|---|
| API p50 latency (any continent) | < 50ms EU, 300ms+ elsewhere | < 50ms everywhere | Application Insights |
| API p99 latency | < 200ms EU | < 150ms everywhere | Application Insights |
| Audio stream start | < 2s EU, 5s+ APAC | < 2s everywhere | Custom metric (player) |
| CDN cache hit ratio | 85-95% | > 95% | Front Door analytics |
| WebSocket reconnect | < 3s | < 3s (with region failover) | Custom metric |
| Upload throughput | ~50 Mbps | ~50 Mbps per region | Load testing |
| Time to first byte (TTFB) | < 100ms EU | < 100ms everywhere | Lighthouse / WebPageTest |
Lessons from Hanteo's Scale
Hanteo Global operates at 15M+ annual users, 1B+ page views, 200+ countries. Key architectural lessons:
- 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.
- 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.
- Fan-scale traffic is spiky — K-POP comeback weeks, award voting, first-week sales campaigns generate massive traffic bursts. Auto-scaling is non-negotiable.
- 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).
- 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
| Tier | Access Cost | Storage Cost | First-Byte Latency | Best For |
|---|---|---|---|---|
| Hot | Low read/write | ~$0.018/GB | Milliseconds | Active projects, recent uploads, profile images |
| Cool | Higher read, lower write | ~$0.010/GB | Milliseconds | Projects not touched in 30+ days, shared links, older versions |
| Cold | Higher read | ~$0.0045/GB | Milliseconds | Archived projects, completed albums, old versions (90+ days) |
| Archive | Highest read | ~$0.002/GB | Hours (rehydration) | Legal holds, compliance copies, raw originals after processing (180+ days) |
Lifecycle Management Policy
{
"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 automaticallyCost Savings Projection
| Scenario | 100 GB stored | 1 TB stored | 10 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 |
| Savings | 62% | 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
uploadscontainer with flat key paths. No separation between originals and processed variants. Images already generatethumb,medium, andoriginalWebP 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 firstWhy Separate Originals from Processed
| Concern | Single Container (current) | Separated Containers (target) |
|---|---|---|
| Lifecycle policies | Can't tier originals differently from processed | Originals auto-archive after 180d, processed stay Hot/Cool |
| Access patterns | All blobs same SAS permissions | Originals: restricted access. Processed: CDN-friendly |
| Backup/DR | Must backup everything identically | Originals: GZRS (max durability). Processed: LRS (re-creatable) |
| Cost | ~$0.018/GB for everything | Originals on Cool/Cold save ~60% |
| Deletion | Accidental delete loses both | Originals have soft-delete + 30-day retention. Processed are re-generable |
| Compliance | Hard to prove data lineage | Originals 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:
# 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 trueThis 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 regionEncryption — At Rest, In Transit, Key Management
Current State vs. Target
| Layer | Current | Target | Why |
|---|---|---|---|
| In transit | ✅ TLS 1.2+ enforced (Azure default) | ✅ Maintain + TLS 1.3 when available | All traffic encrypted in flight |
| At rest (storage) | ✅ SSE with Microsoft-managed keys (default) | CMK via Key Vault | Platform-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 Vault | Required for enterprise compliance (SOC 2, ISO 27001) |
| Application-level | ❌ None | Field-level encryption for PII | Encrypt email, phone, payment tokens before writing to DB |
| Secrets | ⚠️ Env vars in Container Apps | Azure Key Vault with managed identity | Secrets 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
# 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.KeyvaultKey Rotation Strategy
| Key Type | Rotation Frequency | Method | Automation |
|---|---|---|---|
| Storage CMK | Every 90 days | Azure Key Vault auto-rotation | Built-in policy |
| JWT signing secret | Every 90 days | Key Vault + Container App restart | GitHub Actions workflow |
| MongoDB connection string | On compromise only | Atlas credential rotation | Manual (Atlas doesn't auto-rotate) |
| Stripe API key | Annually | Stripe dashboard + Key Vault update | Manual |
| SAS tokens | Generated per-request (short-lived) | Already short-lived in code | Automatic (1-hour tokens for downloads, 1-year for uploads) |
| OAuth client secrets | Annually | Google/Apple console + Key Vault | Manual |
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
// 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# 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/sonnancewebappstorage00Secret Categories & Their Vault Path
| Secret | Key Vault Name | Rotation | Access Scope |
|---|---|---|---|
JWT_SECRET | wavic-jwt-secret | 90 days | Backend only |
MONGODB_URI | wavic-mongodb-uri | On compromise | Backend only |
STRIPE_API_KEY | wavic-stripe-key | Annual | Backend (subscription module) |
STRIPE_WEBHOOK_SECRET | wavic-stripe-webhook | Annual | Backend (webhook endpoint) |
GOOGLE_CLIENT_SECRET | wavic-google-oauth | Annual | Backend (auth module) |
APPLE_CLIENT_SECRET | wavic-apple-oauth | Annual | Backend (auth module) |
SENTRY_DSN | wavic-sentry-dsn | Never | Backend + Frontend |
| Azure Storage | Eliminated — use managed identity | N/A | N/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
| Service | Private Endpoint | Subnet | Public Access After PE |
|---|---|---|---|
| Azure Blob Storage | sonnance-storage-pe | storage-pe | Disabled (Front Door uses Private Link) |
| Azure Key Vault | wavic-kv-pe | storage-pe | Disabled |
| Azure Cache for Redis | wavic-redis-pe | storage-pe | Disabled |
| MongoDB Atlas | Atlas Private Endpoint | db-pe | Disabled (Atlas VPC Peering or Private Link) |
| Azure Front Door | Front Door Private Link origin | frontdoor-pe | Public (it IS the public entry point) |
VPN for Enterprise Clients
Some enterprise clients (labels, distributors) may require VPN connectivity as a security condition:
| VPN Option | What It Does | Cost | Use 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 |
| ExpressRoute | Dedicated private circuit | ~$300+/mo | Large enterprise with strict compliance (rare for SaaS) |
| Azure Bastion | Jump host for admin access | ~$140/mo | SysAdmin access to backend without VPN |
Recommended path: Don't deploy VPN proactively. It's an enterprise add-on:
- Now → IP allowlisting is sufficient
- First enterprise client → Deploy VPN Gateway (P2S) in the VNet
- Large label integration → Site-to-Site VPN for their API integration
- Compliance-heavy client (government/military) → ExpressRoute
Network Security Groups (NSGs)
# 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 InboundBurst & 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
| Event | Expected Spike | Duration | Pattern |
|---|---|---|---|
| Album drop / release day | 10-100x normal | 1-4 hours | Thundering herd: all fans hit "play" simultaneously |
| Live collaboration session | 5-20x normal WebSocket | 1-2 hours | Sustained high WebSocket connections |
| Award voting (future) | 50-500x normal writes | 30-60 min | Write-heavy, requires idempotency |
| Viral social media moment | 10-1000x normal reads | 2-24 hours | Read-heavy, CDN absorbs most |
| Enterprise onboarding | Bulk upload (TB of audio) | Hours-Days | Storage 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
# 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 eventCircuit Breaker Pattern
// 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 readsHanteo Failure Analysis — Lessons Applied
| Hanteo Issue | Root Cause | Sonnance Mitigation |
|---|---|---|
| App crash at 300K concurrent | Likely single-region, no auto-scaling | Multi-region + Container Apps 0→300 auto-scale |
| Voting system overwhelm | Synchronous writes to DB | Queue-based write leveling (async votes) |
| Data inconsistency during spikes | Race conditions on counters | Atomic MongoDB operations ($inc) + idempotent queue consumers |
| Long recovery time | No circuit breakers, cascading failures | Circuit breaker + graceful degradation (stale reads) |
| Repeat occurrences | No pre-warming for predictable events | Event-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
| Layer | Tool | What It Captures | Cost |
|---|---|---|---|
| APM / Traces | Azure Application Insights (or OpenTelemetry → Grafana) | Request traces, dependency maps, exceptions, performance | Free tier → ~$2.30/GB ingested |
| Logs | Azure Log Analytics Workspace | Container logs, audit logs, Key Vault access | Free 5GB/mo → ~$2.76/GB |
| Metrics | Azure Monitor + Prometheus (via Container Apps) | CPU, memory, HTTP 5xx rate, latency percentiles | Included |
| Uptime | Azure Availability Tests (or Checkly, BetterUptime) | Global endpoint checks every 1-5 min | ~$1/test/mo (Azure native) |
| Error tracking | Sentry (already deployed) | Frontend + backend exceptions with stack traces | Free tier (5K events/mo) |
| Real User Monitoring | Vercel Analytics or custom (Application Insights JS SDK) | Page load time, Web Vitals, client errors | Free → $10/mo |
| Cost | Azure Cost Management + budgets | Spend tracking, forecasting, anomaly alerts | Free |
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 reviewSysAdmin Automation — Build vs. Buy
| Operation | Frequency | Manual Effort | Automation Approach | Build/Buy |
|---|---|---|---|---|
| Deploy to all regions | On merge to main | 5 min | GitHub Actions matrix strategy | Build (already have CI/CD) |
| SSL certificate rotation | Every 90 days | 15 min | Azure managed certs (auto) | Buy (Azure built-in) |
| Secret rotation | Every 90 days | 30 min | Key Vault auto-rotation + pipeline | Build (Key Vault policy + GitHub Action) |
| Database backups | Daily | 0 min | MongoDB Atlas automated backups | Buy (Atlas built-in) |
| Cost audit | Weekly | 30 min | Azure Cost Management + budget alerts | Buy (Azure built-in) |
| Vulnerability scanning | On PR + weekly | 10 min | GitHub Dependabot + Docker Scout | Buy (GitHub built-in) |
| Log retention cleanup | Monthly | 0 min | Log Analytics retention policy | Buy (Azure built-in) |
| Scaling decisions | On demand | 15 min | KEDA auto-scaling (Container Apps) | Buy (built into platform) |
| Incident runbooks | On alert | Varies | Automated runbooks (Azure Automation or n8n) | Build |
| Health check monitoring | Continuous | 0 min | Azure Availability Tests / Checkly | Buy |
| Orphan resource cleanup | Monthly | 20 min | Azure Resource Graph query + alert | Build (script) |
| Performance regression | On deploy | 0 min | Load 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
# 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:
# .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 Type | Size Profile | Replication Strategy | RPO | RTO |
|---|---|---|---|---|
| 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) | Tiny | Azure Key Vault auto-replicates within region pair | < 1 min | < 5 min |
| Redis cache | Volatile | No 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
| Component | Backup Method | Frequency | Retention | Recovery |
|---|---|---|---|---|
| MongoDB Atlas | Automated snapshots + continuous backup | Continuous (oplog) | 30 days | Point-in-time restore to any second |
| Blob originals | GZRS auto-replication + soft delete | Continuous | 30-day soft delete + 365-day retention | Restore from paired region or soft-delete |
| Blob processed | Not backed up (re-generate from originals) | N/A | N/A | Re-run processing pipeline |
| Key Vault | Azure-managed replication | Continuous | 90-day purge protection | Auto-recoverable |
| Container Apps | Infrastructure as Code (no state to backup) | On commit | Git history | Re-deploy from IaC |
| DNS / Front Door | Azure-managed (global anyway) | N/A | N/A | Auto-recoverable |
Infrastructure Maturity Roadmap — Summary
Bringing together all sections into a phased execution plan:
| Phase | When | Focus | Key Actions | Monthly Cost Δ |
|---|---|---|---|---|
| 0 — Current | Now | Ship product, iterate fast | Single region, public endpoints, env var secrets, all-Hot storage | ~$11/mo |
| 1 — Harden | Pre-launch | Security & reliability baseline | Key Vault for secrets, managed identity, lifecycle policies on storage, Application Insights, structured alerts | +$20-30/mo |
| 2 — Private | Post-launch | Lock down network, encrypt properly | VNet + private endpoints, CMK encryption, WAF on Front Door, separated containers (originals/processed/temp) | +$40-80/mo |
| 3 — Multi-Region | 10K+ users | Global latency & resilience | Second region (East US 2), MongoDB replicas, multi-origin Front Door, queue-based writes | +$150-200/mo |
| 4 — Enterprise | First enterprise client | Compliance & isolation | VPN Gateway, BYOK encryption, audit logging, SOC 2 prep, SLA commitments | +$100-200/mo |
| 5 — Global | 50K+ users | Full multi-continent | Third+ regions, Global Clusters (zone sharding), auto-scaling per region, fully automated runbooks | +$100-300/mo |
Last Updated: March 2026