Documentation
¶
Overview ¶
Package subscription provides comprehensive SaaS subscription management with resource limits, feature flags, trial periods, and billing provider integration.
The package implements a flexible subscription system that enforces usage limits, controls feature access, and integrates with payment providers (Paddle, Stripe, Lemonsqueezy) through a minimal interface. It's designed for solo developers building SaaS applications who need a pragmatic approach to subscription management.
Architecture ¶
The package follows a service-oriented architecture with clear separation of concerns:
- Service: Main interface providing all subscription operations
- Plan: Defines subscription tiers with limits and features
- BillingProvider: Abstracts payment provider interactions
- SubscriptionStore: Persists subscription data
- ResourceCounterFunc: Tracks resource usage
- PlansListSource: Loads plan definitions
Resource counting is delegated to the application layer through ResourceCounterFunc callbacks, allowing flexible implementation strategies (database aggregates, caching, external services). Plans are cached in memory after loading for optimal performance.
Core Components ¶
The Service interface provides all subscription operations:
- CanCreate: Check resource limits before creation
- GetUsage: Get current usage and limits
- HasFeature: Check feature availability
- CreateCheckoutLink: Generate payment links
- HandleWebhook: Process provider events
Plans define subscription tiers with:
- Resource limits (users, projects, storage, etc.)
- Feature flags (AI, SSO, API access, etc.)
- Trial periods and pricing
- Billing intervals (monthly, annual, none for free)
Quick Start ¶
Create a subscription service with plans, provider, and storage:
import "github.com/dmitrymomot/saaskit/pkg/subscription"
// Define subscription plans
plans := []subscription.Plan{
{
ID: "free",
Name: "Free Tier",
Interval: subscription.BillingIntervalNone,
Limits: map[subscription.Resource]int64{
subscription.ResourceUsers: 1,
subscription.ResourceProjects: 3,
},
},
{
ID: "price_pro_monthly", // Must match provider's price ID
Name: "Professional",
Interval: subscription.BillingIntervalMonthly,
Price: subscription.Money{Amount: 9900, Currency: "USD"},
Limits: map[subscription.Resource]int64{
subscription.ResourceUsers: subscription.Unlimited,
subscription.ResourceProjects: subscription.Unlimited,
},
Features: []subscription.Feature{
subscription.FeatureAI,
subscription.FeatureSSO,
},
TrialDays: 14,
},
}
// Setup counter functions
userCounter := func(ctx context.Context, tenantID uuid.UUID) (int64, error) {
// Count users for this tenant from your database
return db.CountUsers(ctx, tenantID)
}
projectCounter := func(ctx context.Context, tenantID uuid.UUID) (int64, error) {
// Count projects for this tenant
return db.CountProjects(ctx, tenantID)
}
// Create service
svc, err := subscription.NewService(
ctx,
subscription.NewInMemSource(plans...),
provider, // Your BillingProvider implementation
store, // Your SubscriptionStore implementation
subscription.WithCounter(subscription.ResourceUsers, userCounter),
subscription.WithCounter(subscription.ResourceProjects, projectCounter),
)
Paddle Integration ¶
The package includes a complete Paddle implementation. Set up Paddle provider:
import "github.com/dmitrymomot/saaskit/pkg/subscription"
// Configure Paddle
paddleConfig := subscription.PaddleConfig{
APIKey: "your-paddle-api-key",
WebhookSecret: "your-paddle-webhook-secret",
Environment: "sandbox", // or "production"
}
// Create Paddle provider
provider, err := subscription.NewPaddleProvider(paddleConfig)
if err != nil {
log.Fatal("Failed to create Paddle provider:", err)
}
// Use in service creation
svc, err := subscription.NewService(ctx, planSource, provider, store)
Resource Management ¶
Enforce resource limits before allowing resource creation:
// Before creating a user
err := svc.CanCreate(ctx, tenantID, subscription.ResourceUsers)
if errors.Is(err, subscription.ErrLimitExceeded) {
// Show upgrade prompt or reject request
return fmt.Errorf("user limit reached, please upgrade your plan")
}
// Create the user...
user := createUser(...)
// Get current usage and limits
used, limit, err := svc.GetUsage(ctx, tenantID, subscription.ResourceProjects)
if err != nil {
// Handle error
}
fmt.Printf("Using %d of %d projects", used, limit)
// Get usage as percentage for UI progress bars
percentage := svc.GetUsagePercentage(ctx, tenantID, subscription.ResourceStorage)
// Returns 0-100 for normal limits, -1 for unlimited
Counter functions must be fast as they're called frequently. Consider:
- Database indexes on tenant_id columns
- Cached counts with periodic refresh
- Eventual consistency for non-critical resources
Feature Control ¶
Enable/disable features based on subscription plan:
// Check if AI features are available
if svc.HasFeature(ctx, tenantID, subscription.FeatureAI) {
// Enable AI-powered features
result := processWithAI(input)
} else {
// Use basic processing
result := processBasic(input)
}
// Feature checks are fail-closed for security
// Returns false on any error to prevent unauthorized access
Built-in features include:
- FeatureAI: AI-powered capabilities
- FeatureSSO: Single Sign-On integration
- FeatureAPI: API access
- FeatureWebhooks: Webhook functionality
- FeatureAnalytics: Advanced analytics
- And more...
Plan Management ¶
Set plan context for dynamic plan resolution:
import "github.com/dmitrymomot/saaskit/pkg/subscription" // Set plan ID in request context ctx = subscription.SetPlanIDToContext(ctx, "pro_monthly") // Service will use this plan for all operations canCreate := svc.CanCreate(ctx, tenantID, subscription.ResourceUsers)
Alternative: Custom plan resolver from database:
dbResolver := func(ctx context.Context, tenantID uuid.UUID) (string, error) {
return db.GetTenantPlanID(ctx, tenantID)
}
svc, err := subscription.NewService(
ctx, planSource, provider, store,
subscription.WithPlanIDResolver(dbResolver),
)
Checkout and Billing ¶
Create checkout sessions for plan upgrades:
// Create checkout link
link, err := svc.CreateCheckoutLink(ctx, tenantID, "price_pro_monthly",
subscription.CheckoutOptions{
Email: user.Email, // Pre-fill billing email
SuccessURL: "https://app.com/success",
CancelURL: "https://app.com/cancel",
},
)
if err != nil {
// Handle error
}
// Redirect user to hosted checkout
http.Redirect(w, r, link.URL, http.StatusSeeOther)
// Get customer portal link for plan management
portal, err := svc.GetCustomerPortalLink(ctx, tenantID)
if err != nil {
// Handle error (free plans return error as they have no portal)
}
// portal.URL - general portal
// portal.CancelURL - direct to cancellation (if available)
// portal.UpdatePaymentURL - direct to payment update (if available)
Free plans bypass payment processing and activate immediately.
Webhook Processing ¶
Process billing provider webhooks to sync subscription state:
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Process webhook directly with the request
// The provider will extract signature from appropriate header
// (e.g., Paddle-Signature, Stripe-Signature, etc.)
err := svc.HandleWebhook(r)
if err != nil {
log.Printf("Webhook error: %v", err)
http.Error(w, "Webhook processing failed", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
Webhook events automatically update subscription status, plan changes, and trial states in your SubscriptionStore implementation.
Trial Management ¶
Plans can include trial periods that are automatically managed:
// Check if trial is active
sub, err := svc.GetSubscription(ctx, tenantID)
if err != nil {
// Handle error
}
if sub.IsTrialing() {
daysLeft := sub.TrialDaysRemaining()
fmt.Printf("Trial expires in %d days", daysLeft)
// Show trial warning when close to expiration
if daysLeft <= 3 {
showTrialWarning()
}
}
// Verify trial status before allowing access
err = svc.CheckTrial(ctx, tenantID, sub.CreatedAt)
if errors.Is(err, subscription.ErrTrialExpired) {
// Block access and show upgrade prompt
showUpgradePrompt()
return
}
Error Handling ¶
The package defines specific errors for different scenarios:
switch {
case errors.Is(err, subscription.ErrLimitExceeded):
// Resource limit reached - show upgrade prompt
showUpgradePrompt(resource)
case errors.Is(err, subscription.ErrTrialExpired):
// Trial period ended - require plan selection
redirectToPlans()
case errors.Is(err, subscription.ErrPlanNotFound):
// Invalid plan ID - configuration error
log.Error("Invalid plan configuration")
case errors.Is(err, subscription.ErrNoCounterRegistered):
// Counter not registered for resource - configuration error
log.Error("Resource counter not configured")
case errors.Is(err, subscription.ErrSubscriptionNotFound):
// No subscription exists - redirect to plan selection
redirectToPlans()
}
Performance Considerations ¶
For optimal performance:
- Resource counters are called frequently - optimize with indexes and caching
- Plans are cached in memory - plan changes require service restart
- Feature checks are fail-closed - returns false on errors for security
- Database queries should use tenant_id indexes
- Consider read replicas for counter queries
- Cache frequently accessed usage data
Storage Implementation ¶
Implement SubscriptionStore for your database:
type MySubscriptionStore struct {
db *sql.DB
}
func (s *MySubscriptionStore) Get(ctx context.Context, tenantID uuid.UUID) (*subscription.Subscription, error) {
// Query subscription by tenant_id
// Return subscription.ErrSubscriptionNotFound if not found
}
func (s *MySubscriptionStore) Save(ctx context.Context, sub *subscription.Subscription) error {
// Insert or update subscription using tenant_id as primary key
}
The store interface is minimal to support various database systems and ORMs.
Index ¶
- Constants
- Variables
- func GetPlanIDFromContext(ctx context.Context) (string, bool)
- func PlanIDContextResolver(ctx context.Context, _ uuid.UUID) (string, error)
- func SetPlanIDToContext(ctx context.Context, planID string) context.Context
- type BillingInterval
- type BillingProvider
- type CheckoutLink
- type CheckoutOptions
- type CheckoutRequest
- type EventType
- type Feature
- type Money
- type PaddleConfig
- type PaddleProvider
- func (p *PaddleProvider) CreateCheckoutLink(ctx context.Context, req CheckoutRequest) (*CheckoutLink, error)
- func (p *PaddleProvider) GetCustomerPortalLink(ctx context.Context, subscription *Subscription) (*PortalLink, error)
- func (p *PaddleProvider) ParseWebhook(req *http.Request) (*WebhookEvent, error)
- type Plan
- type PlanComparison
- type PlanIDResolver
- type PlansListSource
- type PortalLink
- type Resource
- type ResourceChange
- type ResourceCounterFunc
- type Service
- type ServiceOption
- type Subscription
- type SubscriptionStatus
- type SubscriptionStore
- type UsageInfo
- type WebhookEvent
Constants ¶
const ( DefaultCheckoutExpiry = 24 * time.Hour // Default checkout link expiration DefaultPortalExpiry = 24 * time.Hour // Default portal link expiration FreeCheckoutExpiry = 5 * time.Minute )
Common durations for all billing providers
const ( // Unlimited indicates no limit for a resource (-1 chosen for SQL compatibility) Unlimited int64 = -1 )
Variables ¶
var ( ErrPlanNotFound = errors.New("subscription plan not found") ErrPlanIDNotFound = errors.New("subscription plan ID not found") ErrPlanIDNotInContext = errors.New("subscription plan ID not found in context") ErrInvalidPlanConfiguration = errors.New("invalid subscription plan configuration") ErrLimitExceeded = errors.New("subscription limit exceeded") ErrInvalidResource = errors.New("invalid subscription resource") ErrNoCounterRegistered = errors.New("no usage counter registered for resource") ErrDowngradeNotPossible = errors.New("subscription downgrade not possible") ErrTrialExpired = errors.New("subscription trial has expired") ErrTrialNotAvailable = errors.New("subscription trial not available") ErrSubscriptionNotFound = errors.New("subscription not found") ErrSubscriptionAlreadyExists = errors.New("subscription already exists") ErrInvalidSubscriptionState = errors.New("invalid subscription state") ErrProviderError = errors.New("subscription provider error") ErrFailedToLoadPlans = errors.New("failed to load subscription plans") ErrFailedToCountResourceUsage = errors.New("failed to count resource usage") // Provider-specific errors ErrMissingAPIKey = errors.New("billing provider API key is required") ErrMissingWebhookSecret = errors.New("billing provider webhook secret is required") ErrInvalidProviderEnvironment = errors.New("invalid billing provider environment") ErrWebhookVerificationFailed = errors.New("webhook signature verification failed") ErrNoCheckoutURL = errors.New("no checkout URL returned from provider") ErrNoPortalURL = errors.New("no portal URL returned from provider") ErrMissingProviderCustomerID = errors.New("provider customer ID not available") ErrMissingProviderSubID = errors.New("subscription provider ID is required for customer portal access") ErrMissingTenantID = errors.New("tenant ID is required") ErrMissingPriceID = errors.New("price ID is required") // Webhook processing errors ErrMissingTenantIDInWebhook = errors.New("missing tenant ID in webhook event") ErrWebhookVerification = errors.New("webhook verification error") ErrFailedToReadRequestBody = errors.New("failed to read request body") ErrFailedToParseWebhook = errors.New("failed to parse webhook payload") // Provider operation errors ErrFailedToCreatePaddleClient = errors.New("failed to create paddle client") ErrFailedToCreateTransaction = errors.New("failed to create paddle transaction") ErrFailedToCreatePortalSession = errors.New("failed to create paddle customer portal session") ErrNoPortalForFreePlan = errors.New("no customer portal available for free plans") // Subscription operation errors ErrFailedToSaveSubscription = errors.New("failed to save subscription") ErrFailedToUpdateSubscription = errors.New("failed to update subscription") ErrFailedToCancelSubscription = errors.New("failed to cancel subscription") ErrFailedToUpdateSubscriptionStatus = errors.New("failed to update subscription status") // Configuration errors ErrPlanIDMismatch = errors.New("plan ID mismatch in configuration") ErrNegativeTrialDays = errors.New("plan has negative trial days") )
Functions ¶
func PlanIDContextResolver ¶
PlanIDContextResolver is the default resolver that retrieves plan ID from context. This resolver allows dynamic plan resolution without database lookups, useful for multi-tenant applications where plan ID is determined during request processing.
Types ¶
type BillingInterval ¶
type BillingInterval string
BillingInterval represents the billing frequency for a subscription plan.
const ( BillingIntervalNone BillingInterval = "none" // for free plans BillingIntervalMonthly BillingInterval = "monthly" BillingIntervalAnnual BillingInterval = "annual" )
type BillingProvider ¶
type BillingProvider interface {
// CreateCheckoutLink creates a hosted checkout session
CreateCheckoutLink(ctx context.Context, req CheckoutRequest) (*CheckoutLink, error)
// GetCustomerPortalLink returns a temporary link to the customer portal
// where users can update payment methods, cancel, or change plans.
// The provider implementation decides which fields to use (e.g., Paddle uses TenantID as customer ID)
GetCustomerPortalLink(ctx context.Context, subscription *Subscription) (*PortalLink, error)
// ParseWebhook validates and parses incoming webhook data from HTTP request.
// Must validate signature headers to prevent webhook spoofing attacks.
// Each provider looks for their specific signature headers (e.g., Paddle-Signature).
// Returns normalized event type and raw provider data.
ParseWebhook(r *http.Request) (*WebhookEvent, error)
}
BillingProvider defines the minimal interface for payment provider integrations. This abstraction allows support for different providers (Paddle, Stripe, Lemonsqueezy) while avoiding vendor lock-in. Provider handles all payment complexity through hosted checkouts and customer portals, eliminating PCI compliance concerns.
Implementations should use official provider SDKs and handle provider-specific quirks internally (e.g., Paddle's customer ID mapping, Stripe's metadata fields).
type CheckoutLink ¶
type CheckoutLink struct {
URL string // hosted checkout URL
SessionID string // provider's session identifier
ExpiresAt time.Time // link expiration
}
CheckoutLink represents a hosted checkout session.
type CheckoutOptions ¶
type CheckoutOptions struct {
Email string // pre-fill billing email
SuccessURL string // redirect after successful payment
CancelURL string // redirect if customer cancels
}
CheckoutOptions contains options for creating a checkout session.
type CheckoutRequest ¶
type CheckoutRequest struct {
PriceID string // provider's price/plan identifier
TenantID uuid.UUID // your internal tenant ID
Email string // optional billing email
SuccessURL string // redirect after successful payment
CancelURL string // redirect if customer cancels
}
CheckoutRequest contains data needed to create a checkout session.
type EventType ¶
type EventType string
EventType represents the normalized billing event type. Each provider implementation maps their specific events to these types.
const ( EventSubscriptionCreated EventType = "subscription_created" EventSubscriptionUpdated EventType = "subscription_updated" EventSubscriptionCancelled EventType = "subscription_cancelled" EventSubscriptionResumed EventType = "subscription_resumed" EventPaymentSucceeded EventType = "payment_succeeded" EventPaymentFailed EventType = "payment_failed" )
type Feature ¶
type Feature string
Feature represents a plan-specific capability that can be enabled/disabled.
const ( FeatureAI Feature = "ai" FeatureSSO Feature = "sso" FeatureAPI Feature = "api" FeatureWebhooks Feature = "webhooks" FeatureWhiteLabel Feature = "white_label" FeatureAnalytics Feature = "analytics" FeaturePrioritySupport Feature = "priority_support" FeatureCustomDomain Feature = "custom_domain" FeatureTeamCollaboration Feature = "team_collaboration" FeatureExport Feature = "export" FeatureIntegrations Feature = "integrations" FeatureAuditLog Feature = "audit_log" )
type Money ¶
type Money struct {
Amount int64 // in smallest currency unit (cents for USD)
Currency string // ISO 4217 currency code
}
Money represents a monetary amount in the smallest currency unit. For example, $10.99 USD would be Amount: 1099, Currency: "USD".
type PaddleConfig ¶
type PaddleConfig struct {
APIKey string `env:"PADDLE_API_KEY,required"`
WebhookSecret string `env:"PADDLE_WEBHOOK_SECRET,required"`
Environment string `env:"PADDLE_ENVIRONMENT" envDefault:"production"`
}
PaddleConfig holds configuration for Paddle billing provider.
func (PaddleConfig) Validate ¶
func (c PaddleConfig) Validate() error
Validate checks if the configuration is valid.
type PaddleProvider ¶
type PaddleProvider struct {
// contains filtered or unexported fields
}
PaddleProvider implements BillingProvider for Paddle.
func NewPaddleProvider ¶
func NewPaddleProvider(config PaddleConfig) (*PaddleProvider, error)
NewPaddleProvider creates a new Paddle billing provider.
func (*PaddleProvider) CreateCheckoutLink ¶
func (p *PaddleProvider) CreateCheckoutLink(ctx context.Context, req CheckoutRequest) (*CheckoutLink, error)
CreateCheckoutLink creates a hosted checkout session in Paddle.
func (*PaddleProvider) GetCustomerPortalLink ¶
func (p *PaddleProvider) GetCustomerPortalLink(ctx context.Context, subscription *Subscription) (*PortalLink, error)
GetCustomerPortalLink returns a link to Paddle's customer portal.
func (*PaddleProvider) ParseWebhook ¶
func (p *PaddleProvider) ParseWebhook(req *http.Request) (*WebhookEvent, error)
ParseWebhook validates and parses incoming webhook data from HTTP request.
type Plan ¶
type Plan struct {
ID string // provider's price ID (e.g., price_starter_monthly)
Name string
Description string
Limits map[Resource]int64 // -1 represents unlimited
Features []Feature
Public bool // available for self-service signup
TrialDays int
Price Money
Interval BillingInterval
}
Plan describes a subscription plan and its resource/feature constraints. The ID field should be set to the payment provider's price ID for paid plans to enable direct mapping during checkout and webhook processing.
func (Plan) IsTrialActive ¶
IsTrialActive reports whether the tenant is still in its trial window.
type PlanComparison ¶
type PlanComparison struct {
NewFeatures []Feature
LostFeatures []Feature
IncreasedLimits map[Resource]ResourceChange
DecreasedLimits map[Resource]ResourceChange
NewResources map[Resource]int64
RemovedResources map[Resource]int64
}
PlanComparison contains the differences between two plans. Used to validate downgrades and communicate changes to users.
func ComparePlans ¶
func ComparePlans(current, target *Plan) *PlanComparison
ComparePlans returns the differences between current and target plans.
func (*PlanComparison) HasResourceDecreases ¶
func (c *PlanComparison) HasResourceDecreases() bool
HasResourceDecreases returns true if any resources have decreased limits.
type PlanIDResolver ¶
PlanIDResolver resolves a plan ID for a given tenant.
type PlansListSource ¶
PlansListSource defines how plans are loaded into the subscription service.
func NewInMemSource ¶
func NewInMemSource(plans ...Plan) PlansListSource
NewInMemSource returns an in-memory Source with a deep copy of the given plans. Panics if no plans are provided to ensure the service always has at least one valid plan. Deep copying prevents external modifications from affecting the source's state.
type PortalLink ¶
type PortalLink struct {
URL string // general portal URL (always populated)
CancelURL string // optional: direct to cancellation flow
UpdatePaymentURL string // optional: direct to payment method update
ExpiresAt time.Time // link expiration (usually 24 hours)
}
PortalLink represents a customer portal session with optional action-specific URLs.
type Resource ¶
type Resource string
Resource represents a countable tenant resource type.
const ( ResourceUsers Resource = "users" ResourceProjects Resource = "projects" ResourceAPIKeys Resource = "api_keys" ResourceWebhooks Resource = "webhooks" ResourceEmails Resource = "emails" ResourceTickets Resource = "tickets" ResourceTeamMembers Resource = "team_members" ResourceEnvironments Resource = "environments" ResourceReferrals Resource = "referrals" ResourceCampaigns Resource = "campaigns" ResourceStorage Resource = "storage" // in GB ResourceBandwidth Resource = "bandwidth" // in GB ResourceDomains Resource = "domains" )
type ResourceChange ¶
ResourceChange represents a change in resource limit.
type ResourceCounterFunc ¶
ResourceCounterFunc returns the current usage for a tenant resource. Must be fast and ideally cached as it's called on every resource creation attempt. Consider implementing counters with database aggregates or cached values.
type Service ¶
type Service interface {
// Resource limits and feature checks
CanCreate(ctx context.Context, tenantID uuid.UUID, res Resource) error
GetUsage(ctx context.Context, tenantID uuid.UUID, res Resource) (used, limit int64, err error)
GetUsageSafe(ctx context.Context, tenantID uuid.UUID, res Resource) (used, limit int64)
HasFeature(ctx context.Context, tenantID uuid.UUID, feature Feature) bool
CheckTrial(ctx context.Context, tenantID uuid.UUID, startedAt time.Time) error
VerifyPlan(ctx context.Context, planID string) error
GetUsagePercentage(ctx context.Context, tenantID uuid.UUID, res Resource) int
CanDowngrade(ctx context.Context, tenantID uuid.UUID, targetPlanID string) error
GetAllUsage(ctx context.Context, tenantID uuid.UUID) (map[Resource]UsageInfo, error)
// Subscription management
GetSubscription(ctx context.Context, tenantID uuid.UUID) (*Subscription, error)
// Billing provider interactions
CreateCheckoutLink(ctx context.Context, tenantID uuid.UUID, planID string, opts CheckoutOptions) (*CheckoutLink, error)
GetCustomerPortalLink(ctx context.Context, tenantID uuid.UUID) (*PortalLink, error)
HandleWebhook(r *http.Request) error
}
Service defines the public interface for subscription management.
func NewService ¶
func NewService(ctx context.Context, src PlansListSource, provider BillingProvider, store SubscriptionStore, opts ...ServiceOption) (Service, error)
NewService creates a new Service with the given dependencies. Panics if required parameters (src, provider, store) are nil to fail fast during initialization. This prevents runtime errors from misconfigured services. Use ServiceOption functions to configure optional settings like custom plan ID resolver.
type ServiceOption ¶
type ServiceOption func(*service)
ServiceOption configures a Service instance.
func WithCounter ¶
func WithCounter(resource Resource, fn ResourceCounterFunc) ServiceOption
WithCounter registers a counter function for a specific resource. Counter functions must be fast as they're called on every creation attempt. Panics if a counter for the same resource has already been registered to prevent accidental overwrites and ensure explicit configuration.
func WithPlanIDResolver ¶
func WithPlanIDResolver(resolver PlanIDResolver) ServiceOption
WithPlanIDResolver sets a custom plan ID resolver. Default resolver (PlanIDContextResolver) expects plan ID in context. Use this to implement database-backed plan resolution or other strategies.
type Subscription ¶
type Subscription struct {
TenantID uuid.UUID // primary key - one subscription per tenant
PlanID string
Status SubscriptionStatus
ProviderSubID string // provider's subscription ID (empty for free plans)
ProviderCustomerID string // provider's customer ID (ctm_xxx, cus_xxx, etc)
CreatedAt time.Time
TrialEndsAt *time.Time // set only for plans with trials
UpdatedAt time.Time
CancelledAt *time.Time // set when subscription is cancelled
}
Subscription represents a tenant's subscription to a plan. Each tenant has exactly one active subscription at a time.
func (*Subscription) IsActive ¶
func (s *Subscription) IsActive() bool
func (*Subscription) IsCancelled ¶
func (s *Subscription) IsCancelled() bool
func (*Subscription) IsTrialExpired ¶
func (s *Subscription) IsTrialExpired() bool
func (*Subscription) IsTrialing ¶
func (s *Subscription) IsTrialing() bool
func (*Subscription) TrialDaysRemaining ¶
func (s *Subscription) TrialDaysRemaining() int
TrialDaysRemaining returns the number of days remaining in the trial. Returns 0 if not in trial or trial has expired.
func (*Subscription) TrialDaysRemainingAt ¶
func (s *Subscription) TrialDaysRemainingAt(now time.Time) int
TrialDaysRemainingAt returns the number of days remaining in the trial at a given time. Returns 0 if not in trial or trial has expired. This method is useful for testing with fixed time values.
type SubscriptionStatus ¶
type SubscriptionStatus string
SubscriptionStatus represents the current state of a subscription.
const ( StatusTrialing SubscriptionStatus = "trialing" StatusActive SubscriptionStatus = "active" StatusPastDue SubscriptionStatus = "past_due" StatusCancelled SubscriptionStatus = "cancelled" StatusExpired SubscriptionStatus = "expired" )
type SubscriptionStore ¶
type SubscriptionStore interface {
// Get retrieves a subscription by tenant ID.
// Returns ErrSubscriptionNotFound if no subscription exists.
Get(ctx context.Context, tenantID uuid.UUID) (*Subscription, error)
// Save creates or updates a subscription.
// Implementation should use TenantID to determine if it's an update.
Save(ctx context.Context, subscription *Subscription) error
}
SubscriptionStore defines the interface for subscription persistence. Each tenant has exactly one subscription, so TenantID serves as the primary key.
type WebhookEvent ¶
type WebhookEvent struct {
Type EventType // normalized event type
ProviderEvent string // original provider event name
SubscriptionID string // provider's subscription ID
TenantID uuid.UUID // your internal tenant ID (from custom_data)
CustomerID string // provider's customer ID (ctm_xxx, cus_xxx, etc)
Status string // subscription status
PlanID string // the plan/price they subscribed to
Raw map[string]any // full webhook data
}
WebhookEvent represents a normalized webhook event from the billing provider.