A serverless backend for RAVN Mail license activation and OpenRouter API key management. This service handles license validation through LemonSqueezy, provides trial periods, and manages OpenRouter API keys for AI features.
The service is built on AWS using:
- CloudFront: SSL termination and global CDN for desired domain (ex. activate.ravnmail.com)
- Lambda Functions: Three serverless functions for activate, validate, and webhook endpoints
- DynamoDB: NoSQL database for storing license data and API keys
- SSM Parameter Store: Secure storage for API keys and secrets
- WAF (Optional): Rate limiting and security protection
Starts a new trial period for a user.
Request:
{
"instanceName": "37fb320965936bec5fe5f805154f3b73174f74bb4f5bd86f21c51e3c5188848c",
"email": "user@example.com"
}Response:
{
"instanceId": "abc123",
"instanceName": "37fb320965936bec5fe5f805154f3b73174f74bb4f5bd86f21c51e3c5188848c",
"licenseKey": "trial-8449a605b53f-5480d2ade13c-mrwabv7n",
"user_name": "John Doe",
"user_email": "user@example.com",
"ai_mode": "saas",
"ai_details": {
"token": "sk-or-v1-xxxxx",
"limit": 2,
"limit_remaining": 2,
"expires_at": "2026-02-01T00:00:00Z"
}
}Activates a new license or starts a trial. Creates OpenRouter API key immediately for SaaS licenses.
Request:
{
"instanceName": "37fb320965936bec5fe5f805154f3b73174f74bb4f5bd86f21c51e3c5188848c",
"licenseKey": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}Response:
{
"instanceId": "abc123",
"instanceName": "37fb320965936bec5fe5f805154f3b73174f74bb4f5bd86f21c51e3c5188848c",
"licenseKey": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"user_name": "John Doe",
"user_email": "user@example.com",
"ai_mode": "saas",
"ai_details": {
"token": "sk-or-v1-xxxxx",
"limit": 6,
"limit_remaining": 6,
"expires_at": "2025-02-10T00:00:00Z"
}
}Note: For BYOK mode, ai_details will be undefined as users must provide their own OpenRouter key.
Validates an existing license and only creates a new OpenRouter API key if the old one has expired and the license is still valid.
Request:
{
"licenseKey": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}Response: Same as activate endpoint
Behavior:
- If key is still valid: Returns existing key details with updated
limit_remaining - If key has expired AND license is active: Creates new key with fresh credits
- If license is inactive: Clears
ai_detailsand returns error
Handles LemonSqueezy webhooks for license updates.
Headers:
X-Signature: HMAC-SHA256 signature of the payload
Events Handled:
license_key_createdlicense_key_updated(disables API key if license becomes inactive)- Subscription events (acknowledged but primary handling is through license_key_updated)
- AWS CLI configured with appropriate credentials
- Node.js 22.x or later
- AWS CDK CLI installed:
pnpm install -g aws-cdk - ACM certificate for desired domain (ex. activate.ravnmail.com; must be in us-east-1 for CloudFront)
- LemonSqueezy account with API key
- OpenRouter account with API key
# Install dependencies
pnpm install
# Bootstrap CDK (first time only)
cdk bootstrap aws://ACCOUNT-ID/eu-west-1-
Set the required environment variables:
export CERTIFICATE_ARN="arn:aws:acm:us-east-1:ACCOUNT:certificate/xxxxx" export DOMAIN_NAME="activate.ravnmail.com"
-
Optional: Enable WAF
export ENABLE_WAF=true -
Deploy the stack:
cdk deploy --context certificateArn=$CERTIFICATE_ARN --context domainName=$DOMAIN_NAME
Or with WAF:
cdk deploy --context certificateArn=$CERTIFICATE_ARN --context domainName=$DOMAIN_NAME --context enableWaf=true
Alternatively, use environment variables only:
cdk deploy
-
Configure API keys in SSM Parameter Store:
# Set LemonSqueezy API key aws ssm put-parameter \ --name /ravn/activate/lemonsqueezy-api-key \ --value "YOUR_LEMONSQUEEZY_API_KEY" \ --type SecureString \ --overwrite # Set OpenRouter API key aws ssm put-parameter \ --name /ravn/activate/openrouter-api-key \ --value "YOUR_OPENROUTER_API_KEY" \ --type SecureString \ --overwrite # Set LemonSqueezy webhook secret aws ssm put-parameter \ --name /ravn/activate/webhook-secret \ --value "YOUR_WEBHOOK_SECRET" \ --type SecureString \ --overwrite
-
Configure CloudFront DNS:
- After deployment, note the CloudFront distribution domain name from outputs
- Create a CNAME record in your DNS:
desired domain (ex. activate.ravnmail.com) -> d1234567890abc.cloudfront.net
-
Configure LemonSqueezy Webhook:
- Go to LemonSqueezy Dashboard → Settings → Webhooks
- Add webhook URL:
https://activate.ravnmail.com/v1/webhook - Select events:
license_key_created,license_key_updated - Copy the signing secret and update the SSM parameter
Understanding how OpenRouter keys are managed to prevent credit exploitation:
- User activates license with LemonSqueezy
- OpenRouter API key is created immediately with credits based on plan
- Key expires according to subscription period (monthly keys for subscriptions)
- Key details stored in DynamoDB
- Client calls validation endpoint periodically
- System checks LemonSqueezy license status
- If existing key has NOT expired: Returns existing key with updated usage stats
- If existing key HAS expired AND license is active: Creates new key with fresh credits
- If license is inactive: Clears API key and returns error
- No exploitation: Keys created immediately on activation ensures only legitimate users get credits
- Automatic renewal: Keys auto-renew when expired if subscription is active
- Cost control: One key per active subscription period, not on every validation
- Usage tracking:
limit_remainingshows current credit usage
Edit config/products.json to modify product variants, pricing, and AI credits:
{
"store_id": 37188,
"trial": {
"credits_usd": 2,
"duration_days": 30,
"ai_mode": "saas"
},
"products": {
"ai_subscription": {
"product_id": 697533,
"ai_mode": "saas",
"variants": {
"starter_monthly": {
"variant_id": 1097689,
"credits_usd": 6,
"duration": "monthly"
}
}
}
}
}- Webhook Signature Verification: All LemonSqueezy webhooks are verified using HMAC-SHA256
- SSL/TLS: All traffic uses HTTPS via CloudFront
- Secrets Management: API keys stored in SSM Parameter Store with encryption
- DynamoDB Encryption: All data encrypted at rest with AWS-managed keys
- Point-in-Time Recovery: Enabled on DynamoDB for data protection
- IAM Least Privilege: Lambda functions have minimal required permissions
- CORS: Configured to allow specific headers and methods only
- Rate Limiting (when WAF enabled): 100 requests per 5 minutes per IP
- AWS Managed Rules (when WAF enabled): Protection against common exploits
- Trial Abuse Prevention: One trial per email address tracked in database
- License Validation: All activations verified with LemonSqueezy API
- API Key Rotation: OpenRouter keys automatically renewed monthly
- Expiration Handling: Keys automatically disabled when license expires
- Input Validation: All endpoints validate required fields
- Error Handling: No sensitive information leaked in error messages
- Lambda execution logs:
/aws/lambda/ravn-activate,/aws/lambda/ravn-validate,/aws/lambda/ravn-webhook - Retention: 30 days
- Lambda invocations, errors, duration
- DynamoDB read/write capacity
- CloudFront requests, error rates
- WAF blocked requests (if enabled)
- Lambda error rate > 5%
- DynamoDB throttling
- WAF blocked request rate spike
- CloudFront 5xx error rate > 1%
# Install dependencies
pnpm install
# Compile TypeScript
pnpm run build
# Watch for changes
pnpm run watch
# Run tests
pnpm run test# List stacks
cdk list
# Synthesize CloudFormation template
cdk synth
# Compare deployed stack with current state
cdk diff
# Deploy stack
cdk deploy
# Destroy stack (use with caution!)
cdk destroy-
Certificate Error on Deploy
- Ensure certificate is in us-east-1 region
- Verify certificate ARN is correct
-
Webhook Signature Verification Fails
- Ensure webhook secret in SSM matches LemonSqueezy
- Check X-Signature header is passed through CloudFront
-
OpenRouter Key Creation Fails
- Verify OpenRouter API key in SSM is valid
- Check OpenRouter account has sufficient credits
-
Trial Already Used Error
- Check email-index in DynamoDB for existing trials
- Verify trial_started_at field
This project is licensed under the MIT License. See the LICENSE file for details.