Game Framework
Billing & Usage

Feature Enforcement

Game Framework uses hard-block enforcement - operations are prevented (402 Payment Required) when limits are exceeded, not just warned.

Enforcement Flow

User Attempts Action

User tries to create a package, upload an artifact, or perform an operation.

Handler Calls EnforceFeature

Before executing the operation, the handler checks the feature limit:

if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxPackages); err != nil {
    return errs.PaymentRequired("package limit exceeded")
}

Billing Service Checks Usage

The billing service:

  1. Gets current usage count
  2. Gets plan limit (base + addons)
  3. Compares usage vs limit

Decision

  • If usage >= limit: Return error (operation blocked)
  • If usage < limit: Allow operation to proceed

Error Response

If blocked, return 402 Payment Required with details:

{
  "error": {
    "code": "FEATURE_LIMIT_EXCEEDED",
    "message": "Package limit exceeded",
    "details": {
      "feature": "max_packages",
      "current_usage": 5,
      "limit": 5,
      "plan": "free"
    }
  }
}

Implementation Examples

Package Creation

func (h *PackageHandler) Create(c forge.Context) error {
    ctx := c.Request().Context()
    workspaceID := getWorkspaceID(c)
    
    // Enforce package limit BEFORE creating
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxPackages); err != nil {
        return errs.PaymentRequired("package limit exceeded - upgrade to create more packages")
    }
    
    // Parse request
    var req dto.CreatePackageRequest
    if err := c.Bind(&req); err != nil {
        return errs.BadRequest("invalid request")
    }
    
    // Create package
    pkg, err := h.service.CreatePackage(ctx, &req)
    if err != nil {
        return err
    }
    
    return c.JSON(201, mapper.PackageToDTO(pkg))
}

Version Creation

func (h *VersionHandler) Create(c forge.Context) error {
    ctx := c.Request().Context()
    workspaceID := getWorkspaceID(c)
    
    // Enforce version limit per package
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxVersions); err != nil {
        return errs.PaymentRequired("version limit exceeded for this package")
    }
    
    // Enforce storage limit
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxStorage); err != nil {
        return errs.PaymentRequired("storage limit exceeded")
    }
    
    // Create version...
}

Artifact Upload

func (h *ArtifactHandler) Upload(c forge.Context) error {
    ctx := c.Request().Context()
    workspaceID := getWorkspaceID(c)
    
    // Enforce storage limit
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxStorage); err != nil {
        return errs.PaymentRequired("storage limit exceeded - upgrade or purchase storage addon")
    }
    
    // Upload artifact...
}

Download/Streaming

func (h *StreamingHandler) DownloadChunk(c forge.Context) error {
    ctx := c.Request().Context()
    workspaceID := getWorkspaceID(c)
    
    // Enforce download limit
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxDownloads); err != nil {
        return errs.PaymentRequired("monthly download limit exceeded")
    }
    
    // Enforce bandwidth limit
    if err := h.billing.EnforceFeature(ctx, workspaceID, billing.FeatureKeyMaxBandwidth); err != nil {
        return errs.PaymentRequired("monthly bandwidth limit exceeded")
    }
    
    // Stream chunk...
    
    // Track bandwidth AFTER successful download
    h.billing.TrackFeatureUsage(ctx, workspaceID, billing.FeatureKeyMaxBandwidth, bytesTransferred)
}

Error Responses

402 Payment Required

When a limit is exceeded, the API returns HTTP 402:

HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "error": {
    "code": "FEATURE_LIMIT_EXCEEDED",
    "message": "Package limit exceeded - upgrade to create more packages",
    "details": {
      "feature": "max_packages",
      "current_usage": 5,
      "limit": 5,
      "plan": "free",
      "upgrade_url": "https://app.gameframework.dev/billing/upgrade",
      "addon_available": false
    }
  }
}

With Addon Option

If addons are available for the feature:

{
  "error": {
    "code": "FEATURE_LIMIT_EXCEEDED",
    "message": "Storage limit exceeded",
    "details": {
      "feature": "max_storage",
      "current_usage": 10737418240,
      "limit": 10737418240,
      "plan": "pro",
      "upgrade_url": "https://app.gameframework.dev/billing/upgrade",
      "addon_available": true,
      "addon_key": "extra_storage",
      "addon_price": "$20 per 100GB/month"
    }
  }
}

Client Handling

Detect Limit Exceeded

try {
  const response = await fetch('/v1/packages', {
    method: 'POST',
    body: JSON.stringify(packageData)
  });
  
  if (response.status === 402) {
    const error = await response.json();
    showUpgradePrompt(error.details);
  }
} catch (error) {
  // Handle error
}

Show Upgrade Prompt

function showUpgradePrompt(details) {
  const message = details.addon_available
    ? `${details.message}. Add ${details.addon_key} addon or upgrade your plan.`
    : `${details.message}. Upgrade to ${suggestPlan(details.plan)}.`;
    
  showModal({
    title: 'Limit Exceeded',
    message,
    actions: [
      { label: 'Upgrade Plan', href: details.upgrade_url },
      { label: 'View Addons', href: '/billing/addons' },
      { label: 'Cancel' }
    ]
  });
}

Feature-Specific Enforcement

Limit-Based Features

For count or size limits, enforcement happens before the operation:

// Check BEFORE incrementing
if err := EnforceFeature(ctx, workspaceID, featureKey); err != nil {
    return err // Block operation
}

// Proceed with operation
createResource()

Boolean Features

For boolean features, check if enabled:

// Check if API access is enabled
result, err := billingService.CheckFeatureAccess(ctx, workspaceID, billing.FeatureKeyAPIAccess)
if err != nil || !result.CanProceed {
    return errs.PaymentRequired("API access not available in your plan")
}

// Proceed with API key creation

Best Practices

Check Early: Always enforce feature limits before performing operations, not after.

Clear Messages: Provide clear error messages that explain the limit and how to resolve it.

Track After: For bandwidth/downloads, track usage after successful completion, not before.

Never Skip Enforcement: All operations that consume resources must check limits, no exceptions.

Testing Enforcement

Test Limit Exceeded

func TestPackageCreation_LimitExceeded(t *testing.T) {
    // Create workspace on Free plan (limit: 5 packages)
    workspace := createTestWorkspace(t, "free")
    
    // Create 5 packages (should succeed)
    for i := 0; i < 5; i++ {
        _, err := createPackage(workspace.ID)
        assert.NoError(t, err)
    }
    
    // Try to create 6th package (should fail)
    _, err := createPackage(workspace.ID)
    assert.Error(t, err)
    assert.Equal(t, http.StatusPaymentRequired, getStatusCode(err))
}

Test Addon Capacity

func TestStorage_WithAddon(t *testing.T) {
    // Free plan: 10GB base
    workspace := createTestWorkspace(t, "free")
    
    // Add 2 storage addons (200GB)
    activateAddon(workspace.ID, "extra_storage", 2)
    
    // Total capacity: 210GB
    // Should allow uploads up to 210GB
    err := uploadArtifact(workspace.ID, 200*GB)
    assert.NoError(t, err)
}