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:
- Real Stripe —
POST /billing/checkout→ Stripe Checkout → on success,POST /webhooks/stripereceivescheckout.session.completedand setscompany.is_premium = True,subscription_status,stripe_customer_id. - Simulate payment — when
settings.stripe_enabledis false, the Billing page shows a "Simulate successful payment" button that flipsis_premiumdirectly (via a billing endpoint). Test card for real-Stripe test mode:4242 4242 4242 4242. - 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.