Skip to content

The paywall

Where it is

backend/app/auth.py:

PAYWALL_MESSAGE = (
    "You're viewing demo data. Upgrade to a premium plan to manage your real pipeline."
)

def require_premium_company(db: Session, user: User) -> Company:
    company = get_company(db, user)
    if not company.is_premium:
        raise HTTPException(
            status_code=status.HTTP_402_PAYMENT_REQUIRED, detail=PAYWALL_MESSAGE
        )
    return company

The rule

  • Read paths never call it. Exploring the board, contacts, agenda, reports — all work on a free workspace against the seeded sample data.
  • Every real-data write calls it — creating/editing/deleting contacts, logging interactions, scheduling/completing follow-ups, creating sequences, saving settings, team routing.

If you add a write endpoint, add require_premium_company (usually in place of a plain get_company).

Frontend handling

frontend/src/lib/api.ts has an axios response interceptor: on a 402 it calls into UpgradeContext to open the upgrade modal instead of surfacing an error. PremiumButton / padlocked buttons are a UI convenience — they render locked when company.is_premium is false — but they are not the boundary; the server is.

Flipping a workspace to premium

Three ways:

  1. Real StripePOST /billing/checkout → Stripe Checkout → on success, POST /webhooks/stripe receives checkout.session.completed and sets company.is_premium = True, subscription_status, stripe_customer_id.
  2. Simulate payment — when settings.stripe_enabled is false, the Billing page shows a "Simulate successful payment" button that flips is_premium directly (via a billing endpoint). Test card for real-Stripe test mode: 4242 4242 4242 4242.
  3. Super-admin toggle — the app owner (role == "super_admin") can toggle any workspace's premium flag from /admin/companies (routers/admin.py).

After any of these the frontend just needs a refresh / query invalidation; no re-login.

Testing the gate

# free workspace — should be 402
curl -X POST localhost:8083/contacts \
  -H "Authorization: Bearer dev:demo@navase.example" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Test"}'
# -> 402 {"detail":"You're viewing demo data..."}

Use owner@navase.example (super-admin) to flip demo@navase.example's workspace premium, then repeat — it should 200.