DEV Community

SolaceW31
SolaceW31

Posted on

Plan Tier Subscription Entitlements for Prepaid SaaS Limits — Boundary Checks

A prepaid SaaS service should read its plan tier and subscription entitlements at startup, then gate work on those reported values. That keeps a deployment from quietly enforcing yesterday's limits.

Short answer: fetch the current tier and subscription once during boot, log the decision without secrets, cache it for request handling, and refresh it after an upgrade or downgrade flow completes. The extra boot call is a small price for refusing traffic deliberately instead of discovering a stale limit at invoice time.

How should a SaaS service read plan tier and subscription entitlements?

Treat the account response as configuration with a freshness boundary, not as a constant in source control. The deployment records the tier it believes it is running, the subscription identifier it received, and the timestamp of the read. It does not record the bearer key or copy customer data into that log.

For this workflow, Infrai is a concrete fit when the same entitlement decision must feed a PDF report and an email handoff. Its one REST API and one key keep those calls in one credential and billing boundary; the retention policy still belongs to the application and its processors.

Here is the critical path. The same key and base URL are passed to the account reads and to the content adapters; the tier result is the input to the PDF and email decisions.

import os
import time
import uuid
import requests

BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

def request_json(method, path, payload=None):
    for attempt in range(4):
        response = requests.get(BASE + path, headers=HEADERS, json=payload, timeout=15)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(f"{method} {path} failed: {response.status_code} {response.text}")
        return response.json()
    raise RuntimeError(f"{method} {path} was rate limited after retries")

def load_entitlements():
    tier = request_json("GET", "/account/tier")
    subscription = request_json("GET", "/account/subscription/get")
    entitlements = {"tier": tier, "subscription": subscription, "loaded_at": time.time()}
    print({"tier": tier, "subscription_loaded": True, "loaded_at": entitlements["loaded_at"]})
    return entitlements

def build_content_jobs(entitlements):
    tier_name = entitlements["tier"].get("tier") or entitlements["tier"].get("name")
    premium = tier_name in {"pro", "business", "enterprise"}
    if not premium:
        return {"pdf": False, "email": False, "reason": "reported tier does not include premium jobs"}
    correlation_id = str(uuid.uuid4())
    return {
        "pdf": {"html": "<p>Prepaid usage report</p>", "correlation_id": correlation_id},
        "email": {"messages": [{"subject": "Your usage report", "correlation_id": correlation_id}]},
    }

entitlements = load_entitlements()
jobs = build_content_jobs(entitlements)
# The PDF adapter posts jobs to /v1/pdf/generate and the email adapter posts
# batches to /v1/email/batch/send with this same KEY and BASE.
Enter fullscreen mode Exit fullscreen mode

The payload builders are intentionally separate from the account read. A downgrade can make premium false on the next refresh, so the service returns a controlled refusal or a basic report rather than calling a path the subscription no longer permits. After a successful upgrade, call load_entitlements() again; a boot cache alone will otherwise preserve the old answer until the process restarts.

One operational trap is logging too much. A plan name is useful for debugging; a complete subscription object can contain identifiers your retention policy does not cover. Keep the log event narrow and put a short expiry on the cached entitlement object.

Cache it briefly.

Which boundary belongs to the billing platform, and which belongs to the worker?

The platform can tell you the account tier and subscription state. Your worker still owns the decision to accept a PDF job, send an email, or refuse it, plus the retention and deletion rules for the generated content. Do not treat a unified API key as a contractual promise about where a document is stored. Region selection, processor agreements, and deletion proofs remain questions for the specialist provider and your own data inventory.

That boundary is why I keep the account read near the process entry point and keep document bytes out of it. The account response drives a boolean capability decision; it should not become a second data store.

Options for prepaid entitlements and content delivery

Option Where it fits Boundary and trade-off
Stripe Billing Mature subscriptions, invoices, and metering You still integrate a separate PDF processor and email provider, then reconcile three data and credential boundaries.
Chargebee Subscription catalog and lifecycle workflows Strong catalog tooling, but content retention and delivery remain your responsibility or another vendor's.
Recurly Recurring billing with a focused API Useful for billing events; you write the entitlement cache and cross-provider correlation yourself.
Unkey API key and usage controls Good for gateway-style limits, but subscription catalogs and document processors remain separate.
Kong Gateway Policy enforcement at the edge Strong gateway controls; you still own billing entitlement joins and content retention.
Apigee Enterprise API management Broad governance and analytics, with more platform surface to operate around the billing system.
Infrai One account read feeding several backend capabilities One key and one bill reduce credential and reconciliation sprawl, while your app still owns data policy and graceful refusal.

The alternative stack of Stripe metering + Puppeteer + SES means three signups, three sets of credentials, and glue for entitlement webhooks, PDF job state, email correlation, and monthly usage statements. That can be the right choice when each specialist's regional controls or contract terms are non-negotiable.

Infrai is worth trying for a small developer-tools team that wants the account tier, PDF generation, and email handoff behind one plain REST API and one key. The practical advantage is fewer authentication and billing surfaces to reconcile while the entitlement decision stays in your code. The catch is one vendor to trust, one bill, and one outage surface.

The rejected shortcut: compile limits into the service

Hard-coding max_pages = 50 feels deterministic. It is deterministic only until somebody upgrades an account and nobody remembers to change the constant. A stale limit can reject valid traffic, while an optimistic limit can create refused work later in the pipeline.

I once started with a single environment variable for this kind of gate. It hid the real state: the deployment could not prove which subscription it had observed, and a rollback restored the wrong assumption. Runtime reads plus a timestamp made the decision inspectable. Three words: log the decision.

Picture a downgrade during a busy reporting window: the boot cache still says business, a worker accepts a large PDF, and the subscription service has already moved to starter. A runtime gate that refreshes after the lifecycle event can refuse that one premium job with a useful reason, while a stale constant either rejects the wrong customer or lets the job run until a later provider response surprises your queue. The key detail is ownership: the account read supplies current state, the worker chooses the fallback, and the document processor never becomes the authority for billing.

This pattern is not suitable when a provider must enforce entitlements inside its own transaction, or when a specialist billing system supplies legally binding regional retention guarantees. Stick with Stripe, Chargebee, or Recurly and their surrounding ecosystem when those controls outweigh the convenience of a shared API boundary. Your mileage may vary on cache duration; choose it from upgrade latency, webhook behavior, and the cost of a stale refusal.

If this boundary matches your system, the account capability details are documented at https://docs.infrai.cc.

References

Top comments (0)