Skip to content

Commit 8d47e9f

Browse files
dtfiedlerclaude
andcommitted
feat(billing): add free/premium tier limits for webhooks and monitors
- Free users limited to 1 webhook and 1 monitor - Premium users can have up to 10 webhooks and 10 monitors - Add DISABLE_WEBHOOK_NOTIFICATIONS config option - Update docker-compose to separate API and worker responsibilities - API service now only handles requests, worker handles all notifications - Return 403 with upgrade message when free users hit limits Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a0e46fa commit 8d47e9f

5 files changed

Lines changed: 74 additions & 6 deletions

File tree

docker-compose.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ services:
1818
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
1919
- AWS_REGION=${AWS_REGION:-us-east-1}
2020
- AWS_FROM_EMAIL=${AWS_FROM_EMAIL:-}
21+
# API only handles requests - all notifications handled by worker
2122
- DISABLE_EMAIL_NOTIFICATIONS=true
23+
- DISABLE_WEBHOOK_NOTIFICATIONS=true
2224
- DISABLE_EVENT_PROCESSING=true
25+
- DISABLE_GATEWAY_MONITORING=true
26+
- DISABLE_ARNS_SYNC=true
2327
- ENABLE_HOSTED_FRONTEND=false
2428
- FRONTEND_URL=${FRONTEND_URL:-https://subscribe.permagate.io}
2529
- SECRET_KEY=${SECRET_KEY:-}
@@ -45,7 +49,12 @@ services:
4549
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
4650
- AWS_REGION=${AWS_REGION:-us-east-1}
4751
- AWS_FROM_EMAIL=${AWS_FROM_EMAIL:-}
52+
# Worker handles all notifications and background processing
4853
- DISABLE_EMAIL_NOTIFICATIONS=${DISABLE_EMAIL_NOTIFICATIONS:-false}
54+
- DISABLE_WEBHOOK_NOTIFICATIONS=${DISABLE_WEBHOOK_NOTIFICATIONS:-false}
55+
- DISABLE_EVENT_PROCESSING=${DISABLE_EVENT_PROCESSING:-false}
56+
- DISABLE_GATEWAY_MONITORING=${DISABLE_GATEWAY_MONITORING:-false}
57+
- DISABLE_ARNS_SYNC=${DISABLE_ARNS_SYNC:-false}
4958
- ENABLE_SLACK_NOTIFICATIONS=${ENABLE_SLACK_NOTIFICATIONS:-false}
5059
- SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-}
5160
- ENABLE_DISCORD_NOTIFICATIONS=${ENABLE_DISCORD_NOTIFICATIONS:-false}

src/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export const auth0ClientSecret = process.env.AUTH0_CLIENT_SECRET;
3333
// Email
3434
export const disableEmails = process.env.DISABLE_EMAIL_NOTIFICATIONS === 'true';
3535

36+
// Webhooks (subscriber-defined webhooks)
37+
export const disableWebhookNotifications =
38+
process.env.DISABLE_WEBHOOK_NOTIFICATIONS === 'true';
39+
3640
// Notifications
3741
export const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
3842
export const enableSlackNotifications =
@@ -103,6 +107,21 @@ export const gatewayHealthcheckTimeoutMs = process.env
103107
.GATEWAY_HEALTHCHECK_TIMEOUT_MS
104108
? +process.env.GATEWAY_HEALTHCHECK_TIMEOUT_MS
105109
: 10000;
110+
// Subscriber limits (free vs premium)
111+
export const freeWebhookLimit = process.env.FREE_WEBHOOK_LIMIT
112+
? +process.env.FREE_WEBHOOK_LIMIT
113+
: 1;
114+
export const premiumWebhookLimit = process.env.PREMIUM_WEBHOOK_LIMIT
115+
? +process.env.PREMIUM_WEBHOOK_LIMIT
116+
: 10;
117+
export const freeMonitorLimit = process.env.FREE_MONITOR_LIMIT
118+
? +process.env.FREE_MONITOR_LIMIT
119+
: 1;
120+
export const premiumMonitorLimit = process.env.PREMIUM_MONITOR_LIMIT
121+
? +process.env.PREMIUM_MONITOR_LIMIT
122+
: 10;
123+
124+
// Legacy config - now uses tier-based limits
106125
export const maxMonitorsPerSubscriber = process.env.MAX_MONITORS_PER_SUBSCRIBER
107126
? +process.env.MAX_MONITORS_PER_SUBSCRIBER
108127
: 3;

src/db/sqlite.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,14 @@ export class SqliteDatabase implements SubscriberStore, EventStore {
539539
return deleted > 0;
540540
}
541541

542+
async getWebhookCountForSubscriber(subscriberId: number): Promise<number> {
543+
const result = await this.knex('webhooks')
544+
.where({ subscriber_id: subscriberId })
545+
.count<{ count: string | number }>('* as count')
546+
.first();
547+
return Number(result?.count || 0);
548+
}
549+
542550
// Webhook Events (linking) Methods
543551
async addWebhookEvent(webhookId: number, eventType: string): Promise<void> {
544552
await this.knex<WebhookEventLink>('webhook_events')

src/routes/api.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,25 @@ apiRouter.post(
644644
return res.status(404).json({ error: 'Subscriber not found' });
645645
}
646646

647+
// Check webhook limit based on subscription tier
648+
const { freeWebhookLimit, premiumWebhookLimit } = await import(
649+
'../config.js'
650+
);
651+
const webhookLimit = subscriber.premium
652+
? premiumWebhookLimit
653+
: freeWebhookLimit;
654+
const webhookCount = await req.db.getWebhookCountForSubscriber(
655+
subscriber.id,
656+
);
657+
if (webhookCount >= webhookLimit) {
658+
const upgradeMessage = subscriber.premium
659+
? ''
660+
: ' Upgrade to premium for up to 10 webhooks.';
661+
return res.status(403).json({
662+
error: `Maximum ${webhookLimit} webhook${webhookLimit === 1 ? '' : 's'} allowed for your subscription tier.${upgradeMessage}`,
663+
});
664+
}
665+
647666
const { url, description, type, active, authorization } = req.body;
648667

649668
if (!url || typeof url !== 'string') {
@@ -1527,14 +1546,22 @@ apiRouter.post(
15271546
return res.status(400).json({ error: 'Invalid FQDN format' });
15281547
}
15291548

1530-
// Check monitor limit (max 3 per subscriber)
1531-
const { maxMonitorsPerSubscriber } = await import('../config.js');
1549+
// Check monitor limit based on subscription tier
1550+
const { freeMonitorLimit, premiumMonitorLimit } = await import(
1551+
'../config.js'
1552+
);
1553+
const monitorLimit = subscriber.premium
1554+
? premiumMonitorLimit
1555+
: freeMonitorLimit;
15321556
const monitorCount = await req.db.getMonitorCountForSubscriber(
15331557
subscriber.id,
15341558
);
1535-
if (monitorCount >= maxMonitorsPerSubscriber) {
1536-
return res.status(400).json({
1537-
error: `Maximum ${maxMonitorsPerSubscriber} monitors allowed per subscriber`,
1559+
if (monitorCount >= monitorLimit) {
1560+
const upgradeMessage = subscriber.premium
1561+
? ''
1562+
: ' Upgrade to premium for up to 10 monitors.';
1563+
return res.status(403).json({
1564+
error: `Maximum ${monitorLimit} monitor${monitorLimit === 1 ? '' : 's'} allowed for your subscription tier.${upgradeMessage}`,
15381565
});
15391566
}
15401567

src/system.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const twitterNotifier = config.twitterBearerToken
8686
const webhookNotifier = new WebhookNotificationProvider({
8787
db,
8888
logger,
89-
enabled: true,
89+
enabled: !config.disableWebhookNotifications,
9090
});
9191

9292
// Create composite notification provider with all enabled providers
@@ -216,6 +216,11 @@ async function sendGatewayMonitorWebhook(
216216
alertType: string,
217217
data: MonitorAlertData,
218218
): Promise<void> {
219+
if (config.disableWebhookNotifications) {
220+
logger.debug('Webhook notifications are disabled, skipping gateway monitor webhook');
221+
return;
222+
}
223+
219224
const payload = formatGatewayMonitorWebhookPayload(webhook.type, alertType, data);
220225

221226
const headers: Record<string, string> = {

0 commit comments

Comments
 (0)