|
| 1 | +import { PLAN_IDS, type PlanId } from "./features.js"; |
| 2 | + |
| 3 | +/** |
| 4 | + * Pulse plan tiers - ordered from lowest to highest |
| 5 | + */ |
| 6 | +export const PULSE_PLAN_IDS = { |
| 7 | + FREE: "pulse_free", |
| 8 | + PRO: "pulse_pro", |
| 9 | + BUSINESS: "pulse_business", |
| 10 | +} as const; |
| 11 | + |
| 12 | +export type PulsePlanId = |
| 13 | + (typeof PULSE_PLAN_IDS)[keyof typeof PULSE_PLAN_IDS]; |
| 14 | + |
| 15 | +/** Plan tier hierarchy (index = tier level, higher = more features) */ |
| 16 | +export const PULSE_PLAN_HIERARCHY: PulsePlanId[] = [ |
| 17 | + PULSE_PLAN_IDS.FREE, |
| 18 | + PULSE_PLAN_IDS.PRO, |
| 19 | + PULSE_PLAN_IDS.BUSINESS, |
| 20 | +]; |
| 21 | + |
| 22 | +/** Gated features - locked behind specific plans */ |
| 23 | +export const PULSE_GATED_FEATURES = { |
| 24 | + // Basic checks |
| 25 | + BASIC_UPTIME_CHECKS: "basic_uptime_checks", |
| 26 | + EMAIL_ALERTS: "email_alerts", |
| 27 | + WEBHOOKS: "webhooks", |
| 28 | + PUBLIC_STATUS_PAGE: "public_status_page", |
| 29 | + DASHBOARD_INTEGRATION: "dashboard_integration", |
| 30 | + // Advanced checks |
| 31 | + SSL_CERTIFICATE_CHECKS: "ssl_certificate_checks", |
| 32 | + KEYWORD_CONTENT_MATCH: "keyword_content_match", |
| 33 | + MULTI_LOCATION_CHECKS: "multi_location_checks", |
| 34 | + ONE_MINUTE_FREQUENCY: "one_minute_frequency", |
| 35 | + // Enterprise features |
| 36 | + THIRTY_SECOND_FREQUENCY: "thirty_second_frequency", |
| 37 | + SYNTHETIC_TRANSACTIONS: "synthetic_transactions", |
| 38 | + ALERT_ESCALATION: "alert_escalation", |
| 39 | + SMS_VOICE_ALERTS: "sms_voice_alerts", |
| 40 | + HEARTBEAT_MONITORING: "heartbeat_monitoring", |
| 41 | + N_OUT_OF_M_FALSE_POSITIVE_REDUCTION: "n_out_of_m_false_positive_reduction", |
| 42 | +} as const; |
| 43 | + |
| 44 | +export type PulseGatedFeatureId = |
| 45 | + (typeof PULSE_GATED_FEATURES)[keyof typeof PULSE_GATED_FEATURES]; |
| 46 | + |
| 47 | +/** |
| 48 | + * Plan feature matrix - edit this to control which features are enabled per plan |
| 49 | + */ |
| 50 | +export const PULSE_PLAN_FEATURES: Record< |
| 51 | + PulsePlanId, |
| 52 | + Record<PulseGatedFeatureId, boolean> |
| 53 | +> = { |
| 54 | + [PULSE_PLAN_IDS.FREE]: { |
| 55 | + [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true, |
| 56 | + [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true, |
| 57 | + [PULSE_GATED_FEATURES.WEBHOOKS]: true, |
| 58 | + [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true, |
| 59 | + [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true, |
| 60 | + [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: false, |
| 61 | + [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: false, |
| 62 | + [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: false, |
| 63 | + [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: false, |
| 64 | + [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: false, |
| 65 | + [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: false, |
| 66 | + [PULSE_GATED_FEATURES.ALERT_ESCALATION]: false, |
| 67 | + [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: false, |
| 68 | + [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: false, |
| 69 | + [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: false, |
| 70 | + }, |
| 71 | + [PULSE_PLAN_IDS.PRO]: { |
| 72 | + [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true, |
| 73 | + [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true, |
| 74 | + [PULSE_GATED_FEATURES.WEBHOOKS]: true, |
| 75 | + [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true, |
| 76 | + [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true, |
| 77 | + [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: true, |
| 78 | + [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: true, |
| 79 | + [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: true, |
| 80 | + [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: true, |
| 81 | + [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: false, |
| 82 | + [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: false, |
| 83 | + [PULSE_GATED_FEATURES.ALERT_ESCALATION]: false, |
| 84 | + [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: false, |
| 85 | + [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: false, |
| 86 | + [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: false, |
| 87 | + }, |
| 88 | + [PULSE_PLAN_IDS.BUSINESS]: { |
| 89 | + [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true, |
| 90 | + [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true, |
| 91 | + [PULSE_GATED_FEATURES.WEBHOOKS]: true, |
| 92 | + [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true, |
| 93 | + [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true, |
| 94 | + [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: true, |
| 95 | + [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: true, |
| 96 | + [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: true, |
| 97 | + [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: true, |
| 98 | + [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: true, |
| 99 | + [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: true, |
| 100 | + [PULSE_GATED_FEATURES.ALERT_ESCALATION]: true, |
| 101 | + [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: true, |
| 102 | + [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: true, |
| 103 | + [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: true, |
| 104 | + }, |
| 105 | +}; |
| 106 | + |
| 107 | +/** Plan limits and configuration */ |
| 108 | +export type PulsePlanLimits = { |
| 109 | + /** Number of monitors included */ |
| 110 | + includedMonitors: number; |
| 111 | + /** Check frequency in minutes (or seconds for Business) */ |
| 112 | + checkFrequencyMinutes?: number; |
| 113 | + checkFrequencySeconds?: number; |
| 114 | + /** Data retention period */ |
| 115 | + dataRetentionDays?: number; |
| 116 | + dataRetentionMonths?: number; |
| 117 | + /** Number of check locations for multi-location checks */ |
| 118 | + checkLocations?: number; |
| 119 | +}; |
| 120 | + |
| 121 | +/** Plan metadata including pricing and target audience */ |
| 122 | +export type PulsePlanMetadata = { |
| 123 | + name: string; |
| 124 | + priceUsdMonthly: number; |
| 125 | + targetUser: string; |
| 126 | + limits: PulsePlanLimits; |
| 127 | +}; |
| 128 | + |
| 129 | +/** |
| 130 | + * Plan metadata - pricing, limits, and target audience |
| 131 | + */ |
| 132 | +export const PULSE_PLAN_METADATA: Record<PulsePlanId, PulsePlanMetadata> = { |
| 133 | + [PULSE_PLAN_IDS.FREE]: { |
| 134 | + name: "Pulse Free", |
| 135 | + priceUsdMonthly: 0, |
| 136 | + targetUser: "Hobbyists, Personal Projects", |
| 137 | + limits: { |
| 138 | + includedMonitors: 5, |
| 139 | + checkFrequencyMinutes: 5, |
| 140 | + dataRetentionDays: 30, |
| 141 | + }, |
| 142 | + }, |
| 143 | + [PULSE_PLAN_IDS.PRO]: { |
| 144 | + name: "Pulse Pro", |
| 145 | + priceUsdMonthly: 15, |
| 146 | + targetUser: "SMBs, Small Agencies", |
| 147 | + limits: { |
| 148 | + includedMonitors: 50, |
| 149 | + checkFrequencyMinutes: 1, |
| 150 | + dataRetentionMonths: 12, |
| 151 | + checkLocations: 3, |
| 152 | + }, |
| 153 | + }, |
| 154 | + [PULSE_PLAN_IDS.BUSINESS]: { |
| 155 | + name: "Pulse Business", |
| 156 | + priceUsdMonthly: 49, |
| 157 | + targetUser: "Growing SaaS, Dev Teams", |
| 158 | + limits: { |
| 159 | + includedMonitors: 200, |
| 160 | + checkFrequencySeconds: 30, |
| 161 | + dataRetentionMonths: 24, |
| 162 | + }, |
| 163 | + }, |
| 164 | +}; |
| 165 | + |
| 166 | +type PulseFeatureMeta = { |
| 167 | + name: string; |
| 168 | + description: string; |
| 169 | + upgradeMessage: string; |
| 170 | + minPlan?: PulsePlanId; |
| 171 | +}; |
| 172 | + |
| 173 | +export const PULSE_FEATURE_METADATA: Record< |
| 174 | + PulseGatedFeatureId, |
| 175 | + PulseFeatureMeta |
| 176 | +> = { |
| 177 | + [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: { |
| 178 | + name: "Basic Uptime Checks", |
| 179 | + description: "HTTP/S, Ping, and Port monitoring", |
| 180 | + upgradeMessage: "Basic uptime checks are available on all plans", |
| 181 | + }, |
| 182 | + [PULSE_GATED_FEATURES.EMAIL_ALERTS]: { |
| 183 | + name: "Email Alerts", |
| 184 | + description: "Receive email notifications when monitors go down", |
| 185 | + upgradeMessage: "Email alerts are available on all plans", |
| 186 | + }, |
| 187 | + [PULSE_GATED_FEATURES.WEBHOOKS]: { |
| 188 | + name: "Webhooks", |
| 189 | + description: "Integrate with external services via webhooks", |
| 190 | + upgradeMessage: "Webhooks are available on all plans", |
| 191 | + }, |
| 192 | + [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: { |
| 193 | + name: "Public Status Page", |
| 194 | + description: "Share your service status publicly", |
| 195 | + upgradeMessage: "Public status pages are available on all plans", |
| 196 | + }, |
| 197 | + [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: { |
| 198 | + name: "Dashboard Integration", |
| 199 | + description: "View monitor status in your dashboard", |
| 200 | + upgradeMessage: "Dashboard integration is available on all plans", |
| 201 | + }, |
| 202 | + [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: { |
| 203 | + name: "SSL Certificate Expiry Checks", |
| 204 | + description: "Monitor SSL certificate expiration dates", |
| 205 | + upgradeMessage: "Upgrade to Pro for SSL certificate checks", |
| 206 | + minPlan: PULSE_PLAN_IDS.PRO, |
| 207 | + }, |
| 208 | + [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: { |
| 209 | + name: "Keyword/Content Match", |
| 210 | + description: "Verify specific content appears on monitored pages", |
| 211 | + upgradeMessage: "Upgrade to Pro for keyword/content matching", |
| 212 | + minPlan: PULSE_PLAN_IDS.PRO, |
| 213 | + }, |
| 214 | + [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: { |
| 215 | + name: "Multi-Location Checks", |
| 216 | + description: "Monitor from multiple geographic locations", |
| 217 | + upgradeMessage: "Upgrade to Pro for multi-location checks", |
| 218 | + minPlan: PULSE_PLAN_IDS.PRO, |
| 219 | + }, |
| 220 | + [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: { |
| 221 | + name: "1-Minute Check Frequency", |
| 222 | + description: "Check your monitors every minute", |
| 223 | + upgradeMessage: "Upgrade to Pro for 1-minute check frequency", |
| 224 | + minPlan: PULSE_PLAN_IDS.PRO, |
| 225 | + }, |
| 226 | + [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: { |
| 227 | + name: "30-Second Check Frequency", |
| 228 | + description: "Check your monitors every 30 seconds", |
| 229 | + upgradeMessage: "Upgrade to Business for 30-second check frequency", |
| 230 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 231 | + }, |
| 232 | + [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: { |
| 233 | + name: "Synthetic Transactions", |
| 234 | + description: "Multi-step checks that simulate user workflows", |
| 235 | + upgradeMessage: "Upgrade to Business for synthetic transactions", |
| 236 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 237 | + }, |
| 238 | + [PULSE_GATED_FEATURES.ALERT_ESCALATION]: { |
| 239 | + name: "Alert Escalation Policies", |
| 240 | + description: "Configure alert escalation rules", |
| 241 | + upgradeMessage: "Upgrade to Business for alert escalation", |
| 242 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 243 | + }, |
| 244 | + [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: { |
| 245 | + name: "SMS/Voice Call Alerts", |
| 246 | + description: "Receive alerts via SMS or voice calls via Twilio", |
| 247 | + upgradeMessage: "Upgrade to Business for SMS/voice alerts", |
| 248 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 249 | + }, |
| 250 | + [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: { |
| 251 | + name: "Heartbeat Monitoring", |
| 252 | + description: "Monitor applications that send heartbeat signals", |
| 253 | + upgradeMessage: "Upgrade to Business for heartbeat monitoring", |
| 254 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 255 | + }, |
| 256 | + [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: { |
| 257 | + name: "N-out-of-M False Positive Reduction", |
| 258 | + description: "Reduce false positives by requiring N failures out of M checks", |
| 259 | + upgradeMessage: |
| 260 | + "Upgrade to Business for N-out-of-M false positive reduction", |
| 261 | + minPlan: PULSE_PLAN_IDS.BUSINESS, |
| 262 | + }, |
| 263 | +}; |
| 264 | + |
| 265 | +/** |
| 266 | + * Map Pulse plan to equivalent regular plan for feature checks |
| 267 | + * Pulse Free maps to regular Free plan |
| 268 | + */ |
| 269 | +export function getRegularPlanForPulsePlan( |
| 270 | + pulsePlanId: PulsePlanId | string | null |
| 271 | +): PlanId { |
| 272 | + const pulsePlan = (pulsePlanId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId; |
| 273 | + |
| 274 | + if (pulsePlan === PULSE_PLAN_IDS.FREE) { |
| 275 | + return PLAN_IDS.FREE; |
| 276 | + } |
| 277 | + |
| 278 | + // For other Pulse plans, return free as default |
| 279 | + // You can extend this mapping if needed |
| 280 | + return PLAN_IDS.FREE; |
| 281 | +} |
| 282 | + |
| 283 | +/** Check if a plan has access to a gated feature */ |
| 284 | +export function isPulsePlanFeatureEnabled( |
| 285 | + planId: PulsePlanId | string | null, |
| 286 | + feature: PulseGatedFeatureId |
| 287 | +): boolean { |
| 288 | + const plan = (planId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId; |
| 289 | + return PULSE_PLAN_FEATURES[plan]?.[feature] ?? false; |
| 290 | +} |
| 291 | + |
| 292 | +/** Get the minimum plan required for a feature */ |
| 293 | +export function getMinimumPulsePlanForFeature( |
| 294 | + feature: PulseGatedFeatureId |
| 295 | +): PulsePlanId | null { |
| 296 | + for (const plan of PULSE_PLAN_HIERARCHY) { |
| 297 | + if (PULSE_PLAN_FEATURES[plan][feature]) { |
| 298 | + return plan; |
| 299 | + } |
| 300 | + } |
| 301 | + return null; |
| 302 | +} |
| 303 | + |
| 304 | +/** Get plan metadata */ |
| 305 | +export function getPulsePlanMetadata( |
| 306 | + planId: PulsePlanId | string | null |
| 307 | +): PulsePlanMetadata { |
| 308 | + const plan = (planId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId; |
| 309 | + return PULSE_PLAN_METADATA[plan] ?? PULSE_PLAN_METADATA[PULSE_PLAN_IDS.FREE]; |
| 310 | +} |
| 311 | + |
| 312 | +/** Get plan limits */ |
| 313 | +export function getPulsePlanLimits( |
| 314 | + planId: PulsePlanId | string | null |
| 315 | +): PulsePlanLimits { |
| 316 | + const metadata = getPulsePlanMetadata(planId); |
| 317 | + return metadata.limits; |
| 318 | +} |
| 319 | + |
| 320 | +/** Product information */ |
| 321 | +export const PULSE_PRODUCT_INFO = { |
| 322 | + productName: "Databuddy Pulse", |
| 323 | + coreValueProposition: |
| 324 | + "Integrated, Privacy-First Uptime Monitoring: Know when your site is down, and why, without compromising user privacy.", |
| 325 | +} as const; |
0 commit comments