@lastshotlabs/slingshot-billing
npm install @lastshotlabs/slingshot-billing
Functions
Section titled “Functions”Billing
Section titled “Billing”Public contract for slingshot-billing.
Consumers (e.g. an app mapping plans onto its own domain) read the entitlement
through the typed capability handle rather than reaching into package state.
The package publishes the implementation via capabilities.provides; consumers
resolve it with ctx.capabilities.require(BillingEntitlementCap).
Source: packages/slingshot-billing/src/public.ts
BILLING_PACKAGE_NAME
Section titled “BILLING_PACKAGE_NAME”Stable identifier for this package; used as the contract/event owner name.
Source: packages/slingshot-billing/src/plugin.ts
BillingEntitlementCap
Section titled “BillingEntitlementCap”Public contract for slingshot-billing.
Consumers (e.g. an app mapping plans onto its own domain) read the entitlement
through the typed capability handle rather than reaching into package state.
The package publishes the implementation via capabilities.provides; consumers
resolve it with ctx.capabilities.require(BillingEntitlementCap).
Source: packages/slingshot-billing/src/public.ts
billingPackageConfigSchema
Section titled “billingPackageConfigSchema”Trim trailing slashes and validate a mount path prefix.
Config Fields
Section titled “Config Fields”| Field | Description |
|---|---|
donations | One-time donation configuration. |
mountPath | URL path prefix for billing routes. Must start with ’/’. Default: /billing. |
plans | Subscription plans mapped to provider prices. Empty ⇒ no paid plans. |
provider | Payment provider credentials. Omit to run billing dormant (unconfigured). |
urls | Checkout / Portal redirect URLs. Required once a provider is configured. |
webhookMaxBodyBytes | Maximum body size (bytes) accepted on the Stripe webhook route. Defaults to 1 MiB. |
Source: packages/slingshot-billing/src/types/config.ts
createBillingPackage
Section titled “createBillingPackage”Stable identifier for this package; used as the contract/event owner name.
function createBillingPackage(rawConfig: Partial<BillingPackageConfig> = {}, internals: BillingPackageInternals = {},): SlingshotPackageDefinitionSource: packages/slingshot-billing/src/plugin.ts
createEntityBillingStore
Section titled “createEntityBillingStore”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
function createEntityBillingStore(adapters: BillingEntityAdapters): BillingStoreSource: packages/slingshot-billing/src/lib/store.ts
createStripeProvider
Section titled “createStripeProvider”Stripe implementation of BillingProvider.
The one file allowed to import the Stripe SDK. Everything it returns is
billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no
Stripe type crosses the seam. normalizeStripeEvent is exported separately
(and typed structurally) so tests can pin the event mapping with minimal
fixture objects, no SDK mocking required.
The SDK client is constructed LAZILY on first use: createStripeProvider is
only reachable behind the package’s dormant gate (isBillingConfigured),
and building the provider object itself performs no I/O and no SDK init.
function createStripeProvider(config: StripeProviderConfig): BillingProviderSource: packages/slingshot-billing/src/lib/providers/stripe.ts
deriveEntitlement
Section titled “deriveEntitlement”Pure entitlement derivation: stored subscription rows + configured plans →
the single app-agnostic Entitlement answer.
No I/O, no provider knowledge — lib/sync.ts calls this after every
subscription mutation, and the Phase 5 capability resolver calls it over the
owner’s stored rows.
function deriveEntitlement(rows: readonly BillingSubscriptionRow[], plans: readonly PlanConfig[],): EntitlementSource: packages/slingshot-billing/src/lib/entitlement.ts
entitlementEquals
Section titled “entitlementEquals”Pure entitlement derivation: stored subscription rows + configured plans →
the single app-agnostic Entitlement answer.
No I/O, no provider knowledge — lib/sync.ts calls this after every
subscription mutation, and the Phase 5 capability resolver calls it over the
owner’s stored rows.
function entitlementEquals(a: Entitlement, b: Entitlement): booleanSource: packages/slingshot-billing/src/lib/entitlement.ts
isBillingConfigured
Section titled “isBillingConfigured”Trim trailing slashes and validate a mount path prefix.
function isBillingConfigured(config: BillingPackageConfig): booleanSource: packages/slingshot-billing/src/types/config.ts
normalizeStripeEvent
Section titled “normalizeStripeEvent”Stripe implementation of BillingProvider.
The one file allowed to import the Stripe SDK. Everything it returns is
billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no
Stripe type crosses the seam. normalizeStripeEvent is exported separately
(and typed structurally) so tests can pin the event mapping with minimal
fixture objects, no SDK mocking required.
The SDK client is constructed LAZILY on first use: createStripeProvider is
only reachable behind the package’s dormant gate (isBillingConfigured),
and building the provider object itself performs no I/O and no SDK init.
function normalizeStripeEvent(event: StripeEventLike): ProviderEventSource: packages/slingshot-billing/src/lib/providers/stripe.ts
planKeyForPrice
Section titled “planKeyForPrice”Pure entitlement derivation: stored subscription rows + configured plans →
the single app-agnostic Entitlement answer.
No I/O, no provider knowledge — lib/sync.ts calls this after every
subscription mutation, and the Phase 5 capability resolver calls it over the
owner’s stored rows.
function planKeyForPrice(priceId: string | null | undefined, plans: readonly PlanConfig[],): stringSource: packages/slingshot-billing/src/lib/entitlement.ts
syncProviderEvent
Section titled “syncProviderEvent”Provider-agnostic webhook sync: a normalized ProviderEvent + a
BillingStore → idempotent, order-tolerant persistence + a SyncOutcome
telling the caller (the Phase 4 webhook route) which event to emit.
Invariants this file owns:
- Idempotent: replaying the same event is a no-op (
changed: falsefor subscriptions,'duplicate-payment'for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run. - Order-tolerant: subscription events strictly OLDER than the stored
row’s
providerEventCreatedare DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’screatedhas 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one). - Payments never touch entitlement: a donation inserts a
billing_paymentsrow and nothing else.
async function syncProviderEvent(event: ProviderEvent, store: BillingStore, plans: readonly PlanConfig[],): Promise<SyncOutcome>Source: packages/slingshot-billing/src/lib/sync.ts
Constants
Section titled “Constants”BillingCustomerEntity
Section titled “BillingCustomerEntity”Source: packages/slingshot-billing/src/entities/customer.ts
BillingPaymentEntity
Section titled “BillingPaymentEntity”Source: packages/slingshot-billing/src/entities/payment.ts
BillingSubscriptionEntity
Section titled “BillingSubscriptionEntity”Source: packages/slingshot-billing/src/entities/subscription.ts
FREE_ENTITLEMENT
Section titled “FREE_ENTITLEMENT”Public contract for slingshot-billing.
Consumers (e.g. an app mapping plans onto its own domain) read the entitlement
through the typed capability handle rather than reaching into package state.
The package publishes the implementation via capabilities.provides; consumers
resolve it with ctx.capabilities.require(BillingEntitlementCap).
Source: packages/slingshot-billing/src/public.ts
Interfaces
Section titled “Interfaces”BillingCustomerRow
Section titled “BillingCustomerRow”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingEntitlementChangedPayload
Section titled “BillingEntitlementChangedPayload”Payload for billing:entitlement.changed — an owner’s derived entitlement
actually changed after a verified webhook event was synced.
Delivery is fire-and-forget (the bus swallows and logs handler throws), so
consumers treat this as a cache-invalidation hint and reconcile via
BillingEntitlementCap on read.
Source: packages/slingshot-billing/src/events.ts
BillingEntityAdapter
Section titled “BillingEntityAdapter”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingEntityAdapters
Section titled “BillingEntityAdapters”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingPackageInternals
Section titled “BillingPackageInternals”Stable identifier for this package; used as the contract/event owner name.
Source: packages/slingshot-billing/src/plugin.ts
BillingPaymentCompletedPayload
Section titled “BillingPaymentCompletedPayload”Payload for billing:entitlement.changed — an owner’s derived entitlement
actually changed after a verified webhook event was synced.
Delivery is fire-and-forget (the bus swallows and logs handler throws), so
consumers treat this as a cache-invalidation hint and reconcile via
BillingEntitlementCap on read.
Source: packages/slingshot-billing/src/events.ts
BillingPaymentRow
Section titled “BillingPaymentRow”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingProvider
Section titled “BillingProvider”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
BillingStore
Section titled “BillingStore”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingSubscriptionRow
Section titled “BillingSubscriptionRow”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
CheckoutUrls
Section titled “CheckoutUrls”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
DonationCheckoutInput
Section titled “DonationCheckoutInput”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
Entitlement
Section titled “Entitlement”Public contract for slingshot-billing.
Consumers (e.g. an app mapping plans onto its own domain) read the entitlement
through the typed capability handle rather than reaching into package state.
The package publishes the implementation via capabilities.provides; consumers
resolve it with ctx.capabilities.require(BillingEntitlementCap).
Source: packages/slingshot-billing/src/public.ts
HostedSession
Section titled “HostedSession”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
OwnerRef
Section titled “OwnerRef”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
PortalInput
Section titled “PortalInput”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
ProviderCustomer
Section titled “ProviderCustomer”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
StripeEventLike
Section titled “StripeEventLike”Stripe implementation of BillingProvider.
The one file allowed to import the Stripe SDK. Everything it returns is
billing’s own domain vocabulary (ProviderEvent, HostedSession, …) — no
Stripe type crosses the seam. normalizeStripeEvent is exported separately
(and typed structurally) so tests can pin the event mapping with minimal
fixture objects, no SDK mocking required.
The SDK client is constructed LAZILY on first use: createStripeProvider is
only reachable behind the package’s dormant gate (isBillingConfigured),
and building the provider object itself performs no I/O and no SDK init.
Source: packages/slingshot-billing/src/lib/providers/stripe.ts
SubscriptionCheckoutInput
Section titled “SubscriptionCheckoutInput”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
BillingCustomerInput
Section titled “BillingCustomerInput”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingPackageConfig
Section titled “BillingPackageConfig”Trim trailing slashes and validate a mount path prefix.
Source: packages/slingshot-billing/src/types/config.ts
BillingPaymentInput
Section titled “BillingPaymentInput”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingSubscriptionInput
Section titled “BillingSubscriptionInput”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
BillingSubscriptionPatch
Section titled “BillingSubscriptionPatch”The narrow storage seam billing’s domain logic runs against.
lib/sync.ts and lib/entitlement.ts are pure over BillingStore + plain
row types — they never touch an entity adapter directly. The real
implementation, createEntityBillingStore, is a thin mapping layer over the
framework-resolved entity adapters for billing_customers /
billing_subscriptions / billing_payments.
How the adapters are obtained (the runtime wiring, Phase 4/5): package
entities registered via definePackage({ entities: [entity({ config })] })
get their adapters published into plugin state during the entity bootstrap;
the package resolves them from setupPost onward with
maybeEntityAdapter(app, { plugin: BILLING_PACKAGE_NAME, entity: 'Customer' })
(precedent: slingshot-ai/src/plugin.ts), or captures them at bootstrap via
wiring: { mode: 'factories', onAdapter } (precedent:
slingshot-notifications/src/entities/modules.ts). Either way the adapter
only exists inside setup contexts — which is exactly why this seam exists.
Source: packages/slingshot-billing/src/lib/store.ts
DonationsConfig
Section titled “DonationsConfig”Source: packages/slingshot-billing/src/types/config.ts
EntitlementStatus
Section titled “EntitlementStatus”Public contract for slingshot-billing.
Consumers (e.g. an app mapping plans onto its own domain) read the entitlement
through the typed capability handle rather than reaching into package state.
The package publishes the implementation via capabilities.provides; consumers
resolve it with ctx.capabilities.require(BillingEntitlementCap).
Source: packages/slingshot-billing/src/public.ts
PlanConfig
Section titled “PlanConfig”Source: packages/slingshot-billing/src/types/config.ts
ProviderEvent
Section titled “ProviderEvent”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
StripeProviderConfig
Section titled “StripeProviderConfig”Source: packages/slingshot-billing/src/types/config.ts
SubscriptionStatus
Section titled “SubscriptionStatus”Provider abstraction for slingshot-billing.
All billing routes, sync, and entitlement logic speak ONLY this interface plus
billing’s own domain types — no provider SDK type ever leaks past this seam.
Stripe is the first implementation (lib/providers/stripe.ts, Phase 2); a
second provider slots in by satisfying BillingProvider with no refactor.
Source: packages/slingshot-billing/src/lib/provider.ts
SyncNoopReason
Section titled “SyncNoopReason”Provider-agnostic webhook sync: a normalized ProviderEvent + a
BillingStore → idempotent, order-tolerant persistence + a SyncOutcome
telling the caller (the Phase 4 webhook route) which event to emit.
Invariants this file owns:
- Idempotent: replaying the same event is a no-op (
changed: falsefor subscriptions,'duplicate-payment'for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run. - Order-tolerant: subscription events strictly OLDER than the stored
row’s
providerEventCreatedare DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’screatedhas 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one). - Payments never touch entitlement: a donation inserts a
billing_paymentsrow and nothing else.
Source: packages/slingshot-billing/src/lib/sync.ts
SyncOutcome
Section titled “SyncOutcome”Provider-agnostic webhook sync: a normalized ProviderEvent + a
BillingStore → idempotent, order-tolerant persistence + a SyncOutcome
telling the caller (the Phase 4 webhook route) which event to emit.
Invariants this file owns:
- Idempotent: replaying the same event is a no-op (
changed: falsefor subscriptions,'duplicate-payment'for payments). Stripe retries until it sees a 200, so every path here must tolerate a re-run. - Order-tolerant: subscription events strictly OLDER than the stored
row’s
providerEventCreatedare DROPPED ('stale-event') — Stripe does not guarantee delivery order. Equal timestamps are applied (Stripe’screatedhas 1s granularity; an equal-timestamp replay is the idempotent case, not a stale one). - Payments never touch entitlement: a donation inserts a
billing_paymentsrow and nothing else.
Source: packages/slingshot-billing/src/lib/sync.ts