PrimeCal Subscription System
Overview
PrimeCal's subscription system provides self-service plan management backed by Stripe Billing. Users can subscribe to paid plans, manage billing details, invite team members via seats, and access features gated by their plan tier.
This system is completely separate from the existing Stripe Connect reservation billing — it shares only the STRIPE_SECRET_KEY but uses a different Stripe product (Billing/Checkout vs. Connect).
Architecture
flowchart TD
U[User] -->|selects plan| FE[Frontend /app/subscription]
FE -->|POST /api/subscription/checkout| BE[Backend SubscriptionModule]
BE -->|create checkout session| S[Stripe Billing]
S -->|hosted checkout page| U
U -->|completes payment| S
S -->|webhook event| WH[POST /api/subscription/webhook]
WH -->|activates subscription| DB[(PostgreSQL)]
DB -->|user_entitlements cache| EG[FeatureAccessGuard]
EG -->|allows/blocks| API[Protected API endpoints]
Data Flow
- User browses plans at
/app/subscription - User clicks Subscribe → backend creates Stripe Checkout Session
- User completes payment on Stripe's hosted page
- Stripe fires
checkout.session.completedwebhook - Backend activates subscription, invalidates entitlement cache
- All subsequent API calls check entitlement cache (15-min TTL)
Environment Variables
# Required
STRIPE_SECRET_KEY=sk_live_... # Shared with reservation system
STRIPE_SUBSCRIPTION_WEBHOOK_SECRET=whsec_... # Separate from reservation webhook
STRIPE_PORTAL_RETURN_URL=https://app.primecal.eu/app/subscription
# Optional (with defaults)
ENABLE_SUBSCRIPTIONS=true
SUBSCRIPTION_GRACE_PERIOD_DAYS=7
SEAT_INVITATION_EXPIRY_DAYS=7
ENTITLEMENT_CACHE_TTL_MINUTES=15
Setup
- Run migrations:
cd backend-nestjs && npm run typeorm migration:run - Configure Stripe Dashboard:
- Enable Stripe Tax → add Hungary + EU tax rates
- Customer Portal: allow plan switch, cancellation, billing info, payment methods
- Register webhook:
https://app.primecal.eu/api/subscription/webhook - Copy webhook signing secret to
STRIPE_SUBSCRIPTION_WEBHOOK_SECRET
- System Admin creates plans: Log in as
admin@primecal.local→ navigate to/subscription - Set environment variables and restart backend
User Guide
Free Plan (default)
All new users are automatically assigned to the Free plan with no action required. Free plan:
- 1 seat (owner only)
- Limited features (configured by admin)
- No credit card required
Upgrading
- Navigate to
/app/subscription(via User Menu → Subscription) - Select a plan from the plan picker
- Choose billing period (monthly or yearly)
- Optionally enter a coupon code
- Click Subscribe → redirected to Stripe-hosted checkout
- Complete payment → returned to
/app/subscription?success=true
Managing Billing
Active subscribers can open the Stripe Customer Portal via Manage Billing button on the Current Plan card. From there users can:
- Update payment method
- Download invoices
- Change billing address
- Cancel subscription
Grace Period
When a payment fails, the subscription enters a 7-day grace period. During this time:
- Access continues as normal
- An orange warning banner is shown on
/app/subscription - User must update their payment method via Stripe Portal
- After 7 days without payment, access is revoked
Seat Management
Plans with seat_limit > 1 show the Seat Management panel. The subscription owner can:
- Invite team members by email
- Resend expired invitations (tokens valid 7 days)
- Revoke active seats
Invited members receive an in-app notification with an accept link at /seat-accept?token=....
Billing Profile
Users can save a billing address and VAT ID in the Billing Address section. This information syncs to their Stripe customer record asynchronously (DB is the source of truth; Stripe sync failures are logged but don't block saves).
API Reference
GET /api/subscription/plans
Returns publicly visible plans with pricing. No authentication required for public listing.
GET /api/subscription/current
Returns the authenticated user's current subscription state.
Response:
{
"status": "active",
"planSlug": "pro",
"planName": "Pro",
"billingPeriod": "monthly",
"currentPeriodEnd": "2026-07-25T00:00:00.000Z",
"cancelAtPeriodEnd": false,
"gracePeriodEndsAt": null,
"isSuspended": false,
"seatUsed": 1,
"seatLimit": 5,
"featureSlugs": ["automation", "calendar_sync"]
}
GET /api/subscription/entitlements
Returns the authenticated user's current feature slugs.
POST /api/subscription/checkout
Creates a Stripe Checkout Session.
Request:
{ "planId": 2, "billingPeriod": "monthly", "couponCode": "SAVE20" }
Response:
{ "checkoutUrl": "https://checkout.stripe.com/...", "sessionId": "cs_..." }