PayPal Integration Setup Guide
🚀 Quick Start
Your PayPal integration is now fully working! You can use it in two ways:
Option 1: Legacy Mode (Environment Variables) - Fastest Setup
This is the quickest way to get started. Perfect for single-tenant or testing.
-
Set Environment Variables:
# .env.local PAYPAL_CLIENT_ID=your_paypal_client_id PAYPAL_CLIENT_SECRET=your_paypal_client_secret PAYPAL_MODE=sandbox # or "live" for production # Plan IDs (get these from PayPal or create using the script) PAYPAL_PLAN_PRODUCT_SLUG_MONTHLY=P-XXXXXXXXXXXXXXXXXXXXXX PAYPAL_PLAN_PRODUCT_SLUG_ANNUAL=P-XXXXXXXXXXXXXXXXXXXXXX -
Create PayPal Plans (if you haven't already):
pnpm tsx scripts/create-paypal-plans.ts -
Test Subscription Flow:
- Go to
/portal/subscriptions - Click "Subscribe" on a product
- Complete payment on PayPal sandbox
- You'll be redirected back with an active subscription
- Go to
Option 2: Modular Mode (Per-Organization) - Multi-Tenant
This allows each organization to use their own PayPal account.
-
Set Encryption Key:
# .env.local BILLING_ENCRYPTION_KEY=your-256-bit-encryption-key-here -
Configure Organization Billing Settings:
import { encryptCredentials } from './libs/billing/src/encryption'; import { db } from './libs/database/src/client'; import { organizations } from './libs/database/src/schema'; import { eq } from 'drizzle-orm'; // Encrypt credentials const encryptedCreds = encryptCredentials({ clientId: 'org_paypal_client_id', clientSecret: 'org_paypal_client_secret', webhookId: 'org_webhook_id' // optional, for webhook verification }); // Store in organization metadata await db.update(organizations) .set({ metadata: { billing: { providerType: 'paypal', encryptedCredentials: encryptedCreds, isSandbox: true } } }) .where(eq(organizations.id, organizationId)); -
Test Subscription Flow:
- Same as Option 1, but uses organization's PayPal account
- Each organization can have different payment providers
📋 Database Migration
Run the migration to add provider fields:
# Apply migration
psql $DATABASE_URL -f libs/database/migrations/0002_add_provider_fields.sql
# Or if using Drizzle Kit
pnpm drizzle-kit push:pg🔧 How It Works
Subscription Creation Flow
User clicks "Subscribe"
↓
Check if org has billing settings?
↓
YES → Use PaymentFactory (modular)
↓
- Decrypt org credentials
- Initialize provider (PayPal/Stripe/etc)
- Create subscription
- Redirect to payment URL
NO → Use legacy PayPal (env vars)
↓
- Use PAYPAL_CLIENT_ID from env
- Create PayPal subscription
- Redirect to PayPalSuccess Callback Flow
PayPal redirects to /portal/subscriptions/success?subscription_id=xxx
↓
Find subscription in database
↓
Check if org has billing settings?
↓
YES → Use PaymentFactory to verify
NO → Use legacy PayPal to verify
↓
Update subscription status to "active"
↓
Auto-assign licenses to org members
↓
Redirect to /portal/subscriptions?success=true🎯 Features
✅ Backward Compatible: Existing PayPal integrations work without changes ✅ Multi-Tenant: Each organization can use their own payment provider ✅ Provider Agnostic: Easy to add Stripe, Razorpay, etc. ✅ Secure: Credentials encrypted at rest with AES-256-GCM ✅ Flexible: Supports both sandbox and production modes
🔐 Security Best Practices
-
Never commit credentials to git:
# Add to .gitignore .env.local .env.production -
Use strong encryption key:
# Generate a secure key node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -
Rotate credentials regularly:
- Update PayPal credentials every 90 days
- Update encryption key annually
-
Use sandbox for testing:
- Always test with
PAYPAL_MODE=sandboxfirst - Only switch to
liveafter thorough testing
- Always test with
🐛 Troubleshooting
"Payment plan not configured" Error
Cause: Missing plan ID in environment variables
Solution:
# Check your product slug
echo "Product slug: my-product"
# Expected env var format
PAYPAL_PLAN_MY_PRODUCT_MONTHLY=P-xxx
PAYPAL_PLAN_MY_PRODUCT_ANNUAL=P-xxx"No payment provider configured" Error
Cause: Organization has no billing settings and no env vars
Solution: Either:
- Set
PAYPAL_CLIENT_IDandPAYPAL_CLIENT_SECRETin environment, OR - Configure organization billing settings (see Option 2 above)
Subscription Not Activating
Cause: Webhook not received or subscription verification failed
Solution:
- Check PayPal webhook configuration
- Verify webhook URL is accessible (HTTPS required)
- Check application logs for errors
📚 Additional Resources
🚀 Next Steps
- Run the database migration
- Set up PayPal credentials (Option 1 or 2)
- Create subscription plans using the script
- Test the subscription flow in sandbox mode
- Implement billing settings UI for self-service configuration
✅ What's Working Now
- ✅ PayPal subscription creation
- ✅ Payment URL redirects
- ✅ Success callback handling
- ✅ Subscription activation
- ✅ License auto-assignment
- ✅ Backward compatibility with existing integrations
- ✅ Multi-tenant support (when configured)
🔜 Coming Soon
- Billing settings UI
- Webhook verification
- Stripe provider implementation
- Razorpay provider implementation