Ledger

Introduction

Modern billing engine for SaaS applications in Go.

Ledger is a Go library for building usage-based billing systems for SaaS applications. Instead of managing complex pricing logic, you define plans, track usage, check entitlements, and generate invoices — and Ledger handles the billing orchestration at runtime.

Ledger is a library — not a service. You bring your own database, payment provider, and HTTP server. Ledger provides the billing engine plumbing.

The Billing Model

Ledger models SaaS billing the way modern applications need it:

ComponentWhat it representsGo type
PlanWhat you sell (features + pricing)plan.Plan
FeatureWhat customers can use (seats, API calls, storage)plan.Feature
SubscriptionWhat customers have purchasedsubscription.Subscription
MeterUsage tracking (API calls, storage, etc.)meter.UsageEvent
EntitlementPermission checks (sub-millisecond latency)entitlement.Result
InvoiceWhat customers oweinvoice.Invoice
CouponDiscounts and promotionscoupon.Coupon
ProviderPayment gateway integrationprovider.Provider

What it does

  • Usage-based billing — Track metered usage with high-throughput batched ingestion (10K+ events/sec).
  • Sub-millisecond entitlements — Check feature access with sub-millisecond latency using intelligent caching.
  • Flexible pricing — Graduated tiers, volume pricing, per-seat, flat-rate, and hybrid models.
  • Subscription lifecycle — Trials, upgrades, downgrades, cancellations, and proration.
  • Invoice generation — Automated invoice creation with line items, taxes, and discounts.
  • Multi-tenancy — Built-in tenant isolation with context propagation.
  • Plugin system — 16 lifecycle hooks for metrics, audit trails, webhooks, and custom logic.
  • TypeID everywhere — All entities use type-prefixed, K-sortable identifiers (sub_, inv_, pln_, etc.).
  • Integer-only money — No floating-point precision issues with currency amounts.
  • Multiple stores — PostgreSQL, SQLite, Redis, and in-memory implementations.
  • Provider ready — Integration points for Stripe, Paddle, and custom payment gateways.

Design philosophy

Library, not service. Ledger is a set of Go packages you import. You control main, the database connection, and the process lifecycle.

Performance first. Entitlement checks in under 1ms with caching. Usage ingestion at 10K+ events/second. Invoice generation in under 100ms.

Type-safe money. All currency amounts use integer cents with the types.Money type. No floating-point errors.

Context-driven tenancy. context.Context carries tenant and app IDs, enforced at every layer.

Pluggable everything. Every subsystem defines a Go interface. Swap any storage backend or provider with a single type change.

Quick look

package main

import (
    "context"
    "log"
    "time"

    "github.com/xraph/ledger"
    "github.com/xraph/ledger/plan"
    "github.com/xraph/ledger/store/postgres"
    "github.com/xraph/ledger/subscription"
    "github.com/xraph/ledger/types"
)

func main() {
    ctx := context.Background()

    // Create the store
    store := postgres.New(pool)

    // Build the billing engine
    engine := ledger.New(store,
        ledger.WithMeterConfig(100, 5*time.Second),
        ledger.WithEntitlementCacheTTL(30*time.Second),
    )

    // Start the engine
    if err := engine.Start(ctx); err != nil {
        log.Fatal(err)
    }
    defer engine.Stop()

    // Create a plan
    p := &plan.Plan{
        Name: "Pro Plan",
        Slug: "pro",
        Features: []plan.Feature{
            {
                Key:   "api_calls",
                Type:  plan.FeatureMetered,
                Limit: 10000,
            },
        },
        Pricing: &plan.Pricing{
            BaseAmount: types.USD(4900), // $49.00
        },
    }

    engine.CreatePlan(ctx, p)

    // Create a subscription
    sub := &subscription.Subscription{
        TenantID: "tenant_123",
        PlanID:   p.ID,
        Status:   subscription.StatusActive,
    }

    engine.CreateSubscription(ctx, sub)

    // Check entitlement (sub-millisecond)
    ctx = context.WithValue(ctx, "tenant_id", "tenant_123")
    result, _ := engine.Entitled(ctx, "api_calls")

    if result.Allowed {
        // Track usage (non-blocking)
        engine.Meter(ctx, "api_calls", 1)
    }
}

Where to go next

On this page