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:
- Gets current usage count
- Gets plan limit (base + addons)
- 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 creationBest 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)
}