Policy and Limit Architecture
How Vortos separates authorization, ownership, feature access, rate limits, quotas, sessions, and audit concerns.
Policy and Limit Architecture
Vortos separates security and product enforcement into small, explicit layers. Each layer answers a different question and runs at a different point in the request lifecycle.
Which Primitive Should I Use?
| Need | Use |
|---|---|
| Is this user logged in? | Auth middleware |
| Can this role/action access this resource? | Authorization policy |
| Does this user own this specific resource? | Ownership policy |
| Does this plan/tenant/cohort include this feature? | Feature access |
| Is this client sending requests too quickly? | Rate limit |
| Has this account consumed its business allowance? | Quota |
| How many concurrent sessions may this identity have? | Session policy |
| Should this action be recorded? | Audit logging |
No Backward Compatibility During Alpha
Vortos is pre-stable. Ambiguous generator names are intentionally removed instead of kept as aliases. Use explicit commands such as make:authorization-policy, make:quota-policy, and make:rate-limit-policy.
Request Lifecycle
priority 7: RateLimit IP + Global protects anonymous and global traffic
priority 6: AuthMiddleware validates tokens and sets identity
priority 5: TwoFactorMiddleware enforces second factor when required
priority 4: RateLimit User applies authenticated user buckets
priority 3: AuthorizationMiddleware resource/action permission check
priority 2: OwnershipMiddleware resource owner check
priority 1: FeatureAccessMiddleware product entitlement check
priority 0: QuotaMiddleware business usage allowance checkThis order is deliberate:
- unauthenticated abuse is blocked before auth work
- user buckets wait until identity exists
- unauthorized users do not consume quota
- feature-denied users do not consume quota
- quota is consumed only for requests that passed earlier gates
Generators
php vortos make:authorization-policy Article -c Article
php vortos make:ownership-policy Document -c Document
php vortos make:feature-policy Subscription -c Billing
php vortos make:rate-limit-policy Api -c Billing
php vortos make:quota-policy Billing -c Billing
php vortos make:quota-resolver Organization -c Billing --bucket=organization
php vortos make:session-policy Subscription -c BillingThere is no make:policy command. Use the explicit generator for the policy type you are building.
Enterprise Response Contract
Limit and entitlement failures use machine-readable responses.
| Failure | Status | Content Type |
|---|---|---|
| Feature denied | 403 | application/problem+json |
| Payment required feature | 402 | application/problem+json |
| Quota exceeded | 403 | application/problem+json |
| Rate limit exceeded | 429 | application/problem+json |
Quotas and rate limits use separate headers:
X-Quota-Name
X-Quota-Limit
X-Quota-Remaining
X-Quota-ResetX-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
Retry-AfterObservability
Enforcement happens in middleware, not inside policy classes. Middleware has the request, controller, identity, resolved bucket, response status, and failure reason, so it is the correct place to emit observability.
Metrics are low-cardinality:
feature_access_allowed_total
feature_access_denied_total
rate_limit_allowed_total
rate_limit_blocked_total
quota_allowed_total
quota_blocked_total
quota_consumed_total
http_blocked_totalLogs are emitted for denies, blocks, unresolved subjects, and store failures. Successful requests do not produce logs by default.
Tracing records denial and store-failure events when tracing is configured. Success paths avoid extra spans to keep FrankenPHP worker throughput predictable.
Rejected traffic is also protected from telemetry amplification. Rate-limited requests, security blocks, and basic 404 responses increment a cheap http_blocked_total{reason,status} metric and skip expensive trace enrichment.
Configuration
use Vortos\Auth\Quota\QuotaFailureMode;
return static function (VortosAuthConfig $config): void {
$config
->quotaFailureMode(QuotaFailureMode::FailClosed)
->quotaHeaders(true)
->rateLimitHeaders(true)
->problemDetails(true);
};Failure Mode Guidance
| Mode | Use When | Tradeoff |
|---|---|---|
FailClosed | paid quotas, cost controls, sensitive operations | Redis outage blocks quota-protected requests |
FailOpen | availability-first non-critical allowances | Redis outage bypasses quota temporarily |
Rate limits and quotas require Redis. Redis-backed counters are enforcement counters. For billing-grade usage, also write durable usage events to your application database.
Troubleshooting Checklist
- Run
php vortos list scaffoldingand confirm the expected generator exists. - Run
php vortos list --rawand confirm the runtime command list compiles. - Check Redis is configured when using quotas or rate limits.
- Check controller services are discovered.
- Check policy/resolver classes implement the expected interface.
- Check resolver buckets are lowercase and low-cardinality.
- Check identity claims contain the subject ID your resolver expects.
- Check metrics labels do not include user IDs, emails, request IDs, or raw subject IDs.