Entitlements & Authorization
GospeLib uses a freemium subscription model with three tiers. Entitlements control which features each user can access, enforced at the gateway level.
Plan Tiers
| Tier | Price | Audience | Key Features |
|---|---|---|---|
| Reader (free) | $0 | All members | Scripture reading, basic search, Topical Guide |
| Scholar | $79.99/year | Serious students, educators | + Interlinear, witnesses, scholarly commentary, graph explorer, AI |
| Academic | $149.99/year | Researchers, faculty | + All features, higher rate limits |
Entitlement Map
Entitlements are defined in services/billing/config/plans.yaml:
| Entitlement | Reader | Scholar | Academic |
|---|---|---|---|
scriptures_read | ✅ | ✅ | ✅ |
basic_search | ✅ | ✅ | ✅ |
topical_guide_browse | ✅ | ✅ | ✅ |
interlinear_hebrew_greek | ❌ | ✅ | ✅ |
manuscript_witnesses | ❌ | ✅ | ✅ |
scholarly_commentary | ❌ | ✅ | ✅ |
knowledge_graph_explorer | ❌ | ✅ | ✅ |
cross_references_advanced | ❌ | ✅ | ✅ |
ai_features | ❌ | ✅ | ✅ |
Academic tier has "*" (all entitlements) plus higher rate limits.
Rate Limit Tiers
| Resource | Free | Scholar | Academic |
|---|---|---|---|
| AI requests | 5/hour | 50/hour | 200/hour |
| Search | 20/min | 200/min | 1000/min |
| Passages | 60/min | 600/min | 600/min |
How Entitlements Are Enforced
sequenceDiagram
participant App
participant GW as Gateway
participant Redis
participant Billing as Billing Service
Note over Billing,Redis: At startup + every 60s
Billing->>Redis: Cache entitlements per plan
App->>GW: GET /api/v1/ai/explain
GW->>GW: Extract X-User-Plan from JWT claims
GW->>Redis: GET gl:entitlements:scholar:ai_features
Redis-->>GW: "1" (allowed)
GW->>GW: Proceed to proxy
Key Design Decisions
- Entitlements are cached in Redis (60-second TTL) — the gateway never calls the billing service in the hot path
- Cache key format:
gl:entitlements:<planId>:<feature>→"1"or"0" - O(1) lookup — a single Redis GET per request, not a service call
- Billing service owns the source of truth — plan configurations live in
plans.yamland are pushed to Redis at startup and on subscription changes
Gateway Entitlement Middleware
The gateway uses middleware to gate routes by required entitlement:
r.Group(func(r chi.Router) {
r.Use(entitlement.Require("ai_features"))
r.Mount("/api/v1/ai", proxy.To(cfg.AIServiceURL))
})
If the user's plan doesn't include the required entitlement, the gateway returns a 403 Forbidden with a descriptive error.
Stripe Subscription Mapping
Stripe manages the billing side. Plan changes flow through webhooks:
sequenceDiagram
participant User
participant Stripe
participant Billing as Billing Service
participant PG as PostgreSQL
participant Redis
User->>Stripe: Subscribe to Scholar plan
Stripe->>Billing: Webhook: subscription.created
Billing->>Billing: Verify Stripe signature
Billing->>PG: Check gl_stripe_events (idempotency)
Billing->>PG: INSERT gl_subscriptions
Billing->>PG: UPDATE gl_users SET plan_id = 'scholar'
Billing->>Redis: Refresh entitlement cache
Billing->>PG: INSERT gl_stripe_events (mark processed)
Webhook Idempotency
Every Stripe webhook event is checked against gl_stripe_events before processing. If the stripe_event_id already exists, the webhook returns {"status": "already_processed"} and takes no action.
Stripe Configuration
Plan pricing is stored as config (not hardcoded):
# services/billing/config/plans.yaml
plans:
scholar:
stripe_price_id: '${STRIPE_PRICE_SCHOLAR_MONTHLY}'
stripe_price_annual_id: '${STRIPE_PRICE_SCHOLAR_ANNUAL}'
price_monthly_usd: 799 # $7.99
academic:
stripe_price_id: '${STRIPE_PRICE_ACADEMIC_MONTHLY}'
price_monthly_usd: 1499 # $14.99
Price IDs and product IDs are environment variables — test mode keys in staging, live keys in production.
Related Pages
- Authentication Flow — How X-User-Plan gets injected
- Service Communication — Entitlement middleware in the gateway
- Redis & Caching — Entitlement cache key format and TTL
- PostgreSQL Schema — gl_subscriptions and gl_stripe_events tables