Help us shape the future of Go! Take the 2026 Go Developer Survey

entitlements

package
v0.40.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package entitlements answers "is this account Pro, and why": the rules of Rivolt Pro's plan, in one place.

Rows (table entitlements, one per account and source) say what grants Pro; Compute turns them into a State at a moment in time. Service reads them (cached briefly) and adds the two cases that need no row: billing off (every self-hosted install; everyone is Pro and the table is never read) and the demo account (the showroom is always Pro). Nothing here writes: the rows are written by the billing webhooks, the sign-up trial, the launch-day founding grant and operator grants, each in its own item. Nothing here touches Rivian: the plan is tier-blind to the poller and the shared budget.

Index

Constants

View Source
const (
	SourceApple      = "apple"
	SourceStripe     = "stripe"
	SourceTrial      = "trial"
	SourceFounding   = "founding"
	SourceGrant      = "grant"
	SourceBillingOff = "billing_off" // billing is off on this install: everyone is Pro
	SourceDemo       = "demo"        // the demo account is the showroom
)

Sources a Pro grant can come from, plus the two that need no row.

View Source
const (
	StatusActive  = "active"
	StatusPastDue = "past_due"
	StatusEnded   = "ended"
)

Statuses of a row.

View Source
const DefaultTTL = 15 * time.Second

DefaultTTL bounds how stale a replica's view of an account's rows can be until cross-replica invalidation exists.

View Source
const Grace = 7 * 24 * time.Hour

Grace is how long a paid subscription whose renewal failed stays Pro after its period end.

Variables

View Source
var ErrFoundingCount = errors.New("entitlements: founding rows do not equal the accounts that existed at launch (demo excluded)")

ErrFoundingCount is the count check failing: founding rows are not accounts-at-launch minus the demo.

Functions

func ActiveFreeEnd

func ActiveFreeEnd(rows []Row, now time.Time) (time.Time, bool)

ActiveFreeEnd is the end of the account's free Pro: the latest end among its running founding, grant and trial rows (false: none is running). A new account has none: nothing starts a trial any more, and the 'trial' source is only read, for rows that already exist. A subscription bought while one of them runs must start billing when it ends, not at once (that would bill twice for the same months).

func ActiveTrialEnd

func ActiveTrialEnd(rows []Row, now time.Time) (time.Time, bool)

ActiveTrialEnd is the end of a running trial, whatever else the account has: a Stripe subscription bought during the trial starts billing at that moment (Stripe's trial_end), so the checkout asks for it here rather than from the display fields.

func ApplyFounding

func ApplyFounding(ctx context.Context, d *sql.DB, demo uuid.UUID, launch time.Time) (int64, error)

ApplyFounding grants the founding year to every account that existed at launch (the demo excluded) in one transaction, then checks that founding rows = those accounts, and rolls back if not. Idempotent: an account that already has a founding row keeps it (its end is not moved), so a re-run grants nothing. It returns the number of rows it wrote.

func FoundingEnd

func FoundingEnd(launch time.Time) time.Time

FoundingEnd is how long the founding grant lasts from launch.

func RunFounding

func RunFounding(ctx context.Context, pool *sql.DB, out, errOut io.Writer, demoID uuid.UUID, launch time.Time, apply bool, expectCount int, expectSHA string) int

RunFounding is the body of `rivolt entitlements-founding` (here, not in cmd/rivolt, so a test drives it against Postgres without pulling in the web bundle). It returns the process exit code.

func RunStats added in v0.40.17

func RunStats(ctx context.Context, db *sql.DB, demo uuid.UUID, every time.Duration, report func(Stats))

RunStats reads the counts every interval until ctx ends and hands them to report (the first read is at once). A failed read is skipped: the gauges keep their last values and the next tick tries again.

Types

type Config

type Config struct {
	// Billing is RIVOLT_BILLING: false (the default, every self-hosted install) makes every account Pro without reading the table.
	// Without a database there is nothing to read, so billing counts as off.
	Billing bool
	// Demo is the demo account's id (always Pro with billing on); uuid.Nil for none.
	Demo uuid.UUID
	// TTL is how long an account's rows are cached; 0 means DefaultTTL. A grant written on another replica is seen within it.
	TTL time.Duration
	Log *slog.Logger
	// StripeManage is true when Stripe is configured: only then does a billing-off server look at the rows to tell which accounts still
	// hold a Stripe subscription (ManageSubscription). Every self-hosted install that sells nothing leaves it false and reads no row.
	StripeManage bool
	// OnLookupError is told each time the rows could not be read (the metric beside the fail-open warning).
	OnLookupError func()
}

Config for New.

type FoundingReport

type FoundingReport struct {
	Eligible int // accounts that existed at launch, the demo excluded
	Already  int // of those, how many already have a founding row
	ToGrant  int
	IDs      []uuid.UUID // accounts to grant, sorted
}

FoundingReport is what the grant would do, or did.

func PlanFounding

func PlanFounding(ctx context.Context, q querier, demo uuid.UUID, launch time.Time) (FoundingReport, error)

PlanFounding lists what the founding grant would do for launch. Pure read.

func (FoundingReport) IDsHash

func (r FoundingReport) IDsHash() string

IDsHash is a sha256 over the sorted ids to grant: the dry run prints it and --apply must be given it, so the run that applies is the run that was reviewed.

type Row

type Row struct {
	Source    string
	Status    string
	PeriodEnd *time.Time // nil: no end
	// CancelAtPeriodEnd is a paid subscription set to not renew: still Pro until PeriodEnd (plan_cancels_at).
	CancelAtPeriodEnd bool
	// UpdatedAt is when the row last changed; it dates an ended row that has no period end (a refund, a revoke).
	UpdatedAt time.Time
}

Row is one entitlement row.

type Rowset

type Rowset []Row

Rowset is the cached form: the rows, so a cache hit is recomputed at the current moment (a trial ending is seen on time).

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service reads plans. The zero value is not usable; use New.

func New

func New(db *sql.DB, cfg Config) *Service

New builds a Service over db (the pool the rows are read with).

func (*Service) Billing

func (s *Service) Billing() bool

Billing reports whether billing is on; clients hide every paywall control when it is not.

func (*Service) FreeUntil

func (s *Service) FreeUntil(ctx context.Context, uid uuid.UUID) (time.Time, bool, error)

FreeUntil is the end of the account's running free Pro, a trial or the founding year (false: none). Billing off, the demo account and a failed lookup have none.

func (*Service) Invalidate

func (s *Service) Invalidate(uid uuid.UUID)

Invalidate drops the cached rows of one account on this replica (call it after writing one).

func (*Service) ManageSubscription added in v0.40.17

func (s *Service) ManageSubscription(ctx context.Context, uid uuid.UUID) bool

ManageSubscription is true when billing is off but the account has a Stripe subscription that is not over: it keeps renewing whatever the flag says, so the client keeps showing the way to manage and cancel it (the Customer Portal, which works with billing off). With billing on the plan state says it all, so this is false then, and false for the demo and on a failed lookup.

func (*Service) Pro

func (s *Service) Pro(ctx context.Context, uid uuid.UUID) bool

Pro is whether the account is Pro now. It fails OPEN: if the rows cannot be read, the account counts as Pro, because the cost of that is a free account briefly seeing more, while failing closed would hide a paying user's own history. The failure is logged.

func (*Service) State

func (s *Service) State(ctx context.Context, uid uuid.UUID) (State, error)

State is the account's plan now. The error is the lookup's: callers decide what an unknown plan means (Pro below fails open).

func (*Service) TrialEnd

func (s *Service) TrialEnd(ctx context.Context, uid uuid.UUID) (time.Time, bool, error)

TrialEnd is the end of the account's running trial (false: none). Billing off, the demo account and a failed lookup have none.

func (*Service) Window

func (s *Service) Window(ctx context.Context, uid uuid.UUID) (histwin.W, bool)

Window is the history window that applies to the account now: the free plan's, or none for a Pro account, billing off, the demo account, and (failing open, like Pro) an account whose rows could not be read.

type State

type State struct {
	Pro bool
	// Source is what makes the account Pro: the strongest granting row (a paid one, then founding, grant, trial), billing_off or demo.
	// Empty when free.
	Source string
	// PeriodEnd is when that grant ends, grace included; nil when free or when it has no end.
	PeriodEnd *time.Time
	// TrialEnds is the trial's end, only while the trial is what grants Pro; nil otherwise.
	TrialEnds *time.Time
	// CancelsAt is when a paid subscription that is set to not renew stops granting Pro (its period end): "cancelled, paid until ...".
	// Only for an active stripe or apple row that is the one granting; nil otherwise.
	CancelsAt *time.Time
	// GraceEnds is when the grace of a paid subscription whose renewal failed runs out: "payment failed, update your card by ...".
	// Only while the row granting Pro is a past-due paid one (stripe or apple); nil otherwise.
	GraceEnds *time.Time
	// EndedAt and EndedSource describe a LAPSE: for a free account that once had Pro, when the most recent grant stopped and which source
	// it was ("Your Pro ended on ..."). Nil/empty for an account that never had Pro, and always for a Pro account.
	EndedAt     *time.Time
	EndedSource string
}

State is the plan of one account at one moment.

func Compute

func Compute(rows []Row, now time.Time) State

Compute is the plan from the rows at now. Several rows may grant at once (a trial and a subscription); the strongest source is reported, with its own end, and a client counting down a trial sees TrialEnds only while the trial is what grants.

type Stats added in v0.40.17

type Stats struct {
	ByKind   map[[2]string]int // [source, status] -> rows
	Accounts int
}

Stats is the launch dashboard's numbers, counts only: the entitlement rows by source and status and the accounts, the demo account left out of both. No user id, email or any label that names a person.

func ReadStats added in v0.40.17

func ReadStats(ctx context.Context, db *sql.DB, demo uuid.UUID) (Stats, error)

ReadStats reads the counts (two read-only queries).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL