marketplace

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package marketplace provides a unified framework for two-sided marketplace applications including listings, licenses, and subscriptions.

Index

Constants

View Source
const (
	ResourceTypeListing      = "listing"
	ResourceTypeLicense      = "license"
	ResourceTypeSubscription = "subscription"
	ResourceTypeOrganization = "organization"
	ResourceTypeCreatorOrg   = "creator_org"
	ResourceTypePrincipal    = "principal"
)

SpiceDB resource types for marketplace entities.

View Source
const (
	RelationCreatorOrg   = "creator_org"
	RelationOwner        = "owner"
	RelationLicensedOrg  = "licensed_org"
	RelationListing      = "listing"
	RelationOrganization = "organization"
	RelationPurchasedBy  = "purchased_by"
	RelationSeatHolder   = "seat_holder"
	RelationSubscriber   = "subscriber"
)

SpiceDB relations for marketplace entities.

View Source
const (
	PlanTierFree       = "free"
	PlanTierStarter    = "starter"
	PlanTierPro        = "pro"
	PlanTierEnterprise = "enterprise"
)

PlanTier constants for common subscription tiers.

Variables

View Source
var (
	// ErrListingNotFound is returned when a listing cannot be found.
	ErrListingNotFound = errors.New("listing not found")

	// ErrListingNotPublished is returned when accessing an unpublished listing.
	ErrListingNotPublished = errors.New("listing not published")

	// ErrLicenseNotFound is returned when a license cannot be found.
	ErrLicenseNotFound = errors.New("license not found")

	// ErrLicenseExpired is returned when a license has expired.
	ErrLicenseExpired = errors.New("license expired")

	// ErrLicenseNotYetValid is returned when a license hasn't started.
	ErrLicenseNotYetValid = errors.New("license not yet valid")

	// ErrNoSeatsAvailable is returned when all seats are assigned.
	ErrNoSeatsAvailable = errors.New("no seats available")

	// ErrSeatAlreadyAssigned is returned when a user already has a seat.
	ErrSeatAlreadyAssigned = errors.New("seat already assigned to this user")

	// ErrSeatNotAssigned is returned when trying to unassign a non-existent seat.
	ErrSeatNotAssigned = errors.New("seat not assigned to this user")

	// ErrSubscriptionNotFound is returned when a subscription cannot be found.
	ErrSubscriptionNotFound = errors.New("subscription not found")

	// ErrSubscriptionInactive is returned when a subscription is not active.
	ErrSubscriptionInactive = errors.New("subscription not active")

	// ErrInvalidRevenueShare is returned when revenue share doesn't add to 100%.
	ErrInvalidRevenueShare = errors.New("revenue share must add up to 100%")

	// ErrInvalidPricingModel is returned for unknown pricing models.
	ErrInvalidPricingModel = errors.New("invalid pricing model")

	// ErrInvalidLicenseType is returned for unknown license types.
	ErrInvalidLicenseType = errors.New("invalid license type")

	// ErrAlreadyLicensed is returned when an org already has a license.
	ErrAlreadyLicensed = errors.New("organization already licensed for this listing")

	// ErrManagedPurchaser is returned when a managed principal (a human
	// without an email, ADR-005) tries to purchase; billing is a guardian's
	// action.
	ErrManagedPurchaser = errors.New("managed principals cannot purchase")

	// ErrCannotPurchaseOwnListing is returned when a creator tries to purchase their own listing.
	ErrCannotPurchaseOwnListing = errors.New("cannot purchase your own listing")

	// ErrPaymentRequired is returned when payment is required but not provided.
	ErrPaymentRequired = errors.New("payment required")

	// ErrPaymentFailed is returned when payment processing fails.
	ErrPaymentFailed = errors.New("payment failed")
)

Sentinel errors for marketplace operations.

View Source
var DefaultPurchaseRoles = []string{"owner", "admin"}

DefaultPurchaseRoles are the organization roles that may buy for it: the roles that manage the organization.

View Source
var ErrNoCaller = errors.New("marketplace: no authenticated caller")

ErrNoCaller is returned when a request carries no authenticated principal that the marketplace can act for.

View Source
var MarketplaceSchema string

MarketplaceSchema is the marketplace's SpiceDB schema fragment. It does not stand alone: it attaches to the "principal" and "organization" definitions of authz.BaseSpiceDBSchema through the marketplace_org facet, as the vocabulary rules require. Use MergeSchema to compose it.

Functions

func ApproleOptions added in v0.14.0

func ApproleOptions(src organization.EntitlementSource) []approle.Option

ApproleOptions returns the approle.Service options that make src the source of which applications an organization may assign roles in, and how many seats each has. An application is entitled when any entitlement names it. Its seat limit is the sum of the seats of its seat-based licenses, and none when any entitlement for it is not seat-limited (a subscription, an unlimited license, a default).

func EntitlementSource added in v0.14.0

func EntitlementSource(subs SubscriptionService, licenses LicenseService, listings ListingService, opts ...EntitlementOption) organization.EntitlementSource

EntitlementSource returns an organization.EntitlementSource that derives an organization's entitlements from its subscriptions, its valid licenses and the deployment's default entitlements.

A subscription or license grants an application when its listing names one (Listing.Application). The organization-wide platform subscription has no listing and grants none; it is reported for its tier. A seat-based license carries its seat count. Any of subs, licenses and listings may be nil to leave that source out; without listings no subscription or license grants an application. subs that also implements SubscriptionLister contributes every subscription of the organization, otherwise only its platform subscription.

func MergeSchema

func MergeSchema(appSchema string) string

MergeSchema composes the SystemForge base schema, the marketplace schema and an application-specific schema. The app schema should define app-specific resource types that reference marketplace types (listing, license, etc.).

func PlanTiers

func PlanTiers() []string

PlanTiers returns all standard plan tiers.

func RequestPrincipalCaller added in v0.14.0

func RequestPrincipalCaller(ctx context.Context) (uuid.UUID, error)

RequestPrincipalCaller resolves the caller from requestprincipal, as set by requestprincipal.Middleware. Principals whose ID is not a SystemForge principal ID, such as issuer-qualified subjects of credentials issued elsewhere, cannot act in the marketplace.

func WarnIfNoEntitlements added in v0.14.0

func WarnIfNoEntitlements(ctx context.Context, logger *slog.Logger, listings ListingService, registered []string, opts ...EntitlementOption) (bool, error)

WarnIfNoEntitlements logs a warning when the deployment registers applications but nothing can entitle an organization to them: no default entitlement and no listing that names an application. Every organization is then refused every application role (ADR-007). Call it at startup with the registered application identifiers; it reports whether it warned.

Types

type API

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

API handles marketplace HTTP endpoints.

func NewAPI

func NewAPI(humaAPI huma.API, cfg APIConfig, svc Service, opts ...APIOption) *API

NewAPI creates a new marketplace API handler. Checkout acts for the authenticated caller (see WithCallerResolver) and requires a PurchaseAuthorizer (see WithPurchaseAuthorizer).

type APIConfig

type APIConfig struct {
	// BasePath is the API base path (e.g., "/api/v1").
	BasePath string
}

APIConfig holds API configuration.

type APIOption added in v0.14.0

type APIOption func(*API)

APIOption configures an API.

func WithAuthorizer added in v0.14.0

func WithAuthorizer(a authz.Authorizer) APIOption

WithAuthorizer sets the authorizer the API checks organization permissions with (authz.PermOrgView, PermOrgEdit, PermOrgManage on the listing's creator organization or the license's organization). Until one is configured, every organization-scoped operation is refused; only published listings can be read.

func WithCallerResolver added in v0.14.0

func WithCallerResolver(r CallerResolver) APIOption

WithCallerResolver sets how the API identifies the calling principal. Defaults to RequestPrincipalCaller.

func WithPurchaseAuthorizer added in v0.14.0

func WithPurchaseAuthorizer(p PurchaseAuthorizer) APIOption

WithPurchaseAuthorizer sets who may buy for an organization, for example OrgRolePurchaseAuthorizer(orgAuthorizer). Without one, buying requires authz.PermOrgManage through the authorizer set by WithAuthorizer; with neither, checkout is refused.

type ArchiveListingInput

type ArchiveListingInput struct {
	ID string `path:"id" format:"uuid"`
}

ArchiveListingInput is the request for archiving a listing.

type ArchiveListingOutput

type ArchiveListingOutput struct {
	Body *ListingResponse
}

ArchiveListingOutput is the response for archiving a listing.

type AssignSeatInput

type AssignSeatInput struct {
	LicenseID string `path:"license_id" format:"uuid"`
	Body      struct {
		PrincipalID string `json:"principal_id" required:"true" format:"uuid"`
		AssignedBy  string `` /* 157-byte string literal not displayed */
	}
}

AssignSeatInput is the request for assigning a seat.

type AssignSeatOutput

type AssignSeatOutput struct {
	Body *SeatAssignmentResponse
}

AssignSeatOutput is the response for assigning a seat.

type AuthzSyncer

type AuthzSyncer interface {
	// SyncListing syncs a listing to the authorization system.
	SyncListing(ctx context.Context, listing *Listing) error

	// SyncLicense syncs a license grant to the authorization system.
	SyncLicense(ctx context.Context, license *License) error

	// SyncLicenseRevocation syncs a license revocation to the authorization system.
	SyncLicenseRevocation(ctx context.Context, license *License) error

	// SyncSeatAssignment syncs a seat assignment to the authorization system.
	SyncSeatAssignment(ctx context.Context, assignment *SeatAssignment) error

	// SyncSeatUnassignment syncs a seat removal to the authorization system.
	SyncSeatUnassignment(ctx context.Context, licenseID, principalID uuid.UUID) error
}

AuthzSyncer syncs marketplace entities to the authorization system.

type CallerResolver added in v0.14.0

type CallerResolver func(ctx context.Context) (uuid.UUID, error)

CallerResolver returns the principal making a request. It returns ErrNoCaller when the request is unauthenticated.

type CheckoutRequest

type CheckoutRequest struct {
	// ListingID is the listing to purchase.
	ListingID uuid.UUID

	// OrganizationID is the purchasing organization.
	OrganizationID uuid.UUID

	// PurchaserID is the principal making the purchase.
	PurchaserID uuid.UUID

	// Seats is the number of seats (for per-seat pricing).
	Seats *int

	// SuccessURL is the redirect URL on success.
	SuccessURL string

	// CancelURL is the redirect URL on cancel.
	CancelURL string
}

CheckoutRequest contains parameters for creating a checkout session.

type CheckoutService

type CheckoutService interface {
	// CreateCheckoutSession creates a Stripe checkout session for a listing.
	CreateCheckoutSession(ctx context.Context, req CheckoutRequest) (*CheckoutSession, error)

	// ProcessWebhook handles Stripe webhook events.
	ProcessWebhook(ctx context.Context, payload []byte, signature string) error
}

CheckoutService provides operations for purchasing licenses.

type CheckoutSession

type CheckoutSession struct {
	// SessionID is the Stripe checkout session ID.
	SessionID string

	// URL is the checkout URL to redirect the user to.
	URL string
}

CheckoutSession contains the result of creating a checkout session.

type CreateCheckoutInput

type CreateCheckoutInput struct {
	Body struct {
		ListingID      string `json:"listing_id" required:"true" format:"uuid"`
		OrganizationID string `json:"organization_id" required:"true" format:"uuid"`
		// PurchaserID is deprecated: the purchaser is the authenticated
		// caller. When present it must name the caller.
		PurchaserID string `` /* 159-byte string literal not displayed */
		Seats       *int   `json:"seats,omitempty" minimum:"1"`
		SuccessURL  string `json:"success_url" required:"true" format:"uri"`
		CancelURL   string `json:"cancel_url" required:"true" format:"uri"`
	}
}

CreateCheckoutInput is the request for creating a checkout session.

type CreateCheckoutOutput

type CreateCheckoutOutput struct {
	Body struct {
		SessionID string `json:"session_id"`
		URL       string `json:"url"`
	}
}

CreateCheckoutOutput is the response for creating a checkout session.

type CreateListingInput

type CreateListingInput struct {
	Body struct {
		CreatorOrgID string         `json:"creator_org_id" required:"true" format:"uuid"`
		OwnerID      string         `` /* 151-byte string literal not displayed */
		ProductType  string         `json:"product_type" required:"true" minLength:"1" maxLength:"50"`
		ProductID    *string        `json:"product_id,omitempty" format:"uuid"`
		Title        string         `json:"title" required:"true" minLength:"1" maxLength:"200"`
		Description  string         `json:"description,omitempty" maxLength:"5000"`
		PricingModel string         `json:"pricing_model" required:"true" enum:"free,one_time,subscription,per_seat"`
		PriceCents   int64          `json:"price_cents" minimum:"0"`
		Currency     string         `json:"currency" required:"true" minLength:"3" maxLength:"3" default:"USD"`
		Metadata     map[string]any `json:"metadata,omitempty"`
	}
}

CreateListingInput is the request for creating a listing.

type CreateListingOutput

type CreateListingOutput struct {
	Body *ListingResponse
}

CreateListingOutput is the response for creating a listing.

type DeleteListingInput

type DeleteListingInput struct {
	ID string `path:"id" format:"uuid"`
}

DeleteListingInput is the request for deleting a listing.

type EntLicenseService

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

EntLicenseService is an Ent-backed implementation of LicenseService.

func NewEntLicenseService

func NewEntLicenseService(client *ent.Client, authzSync AuthzSyncer) *EntLicenseService

NewEntLicenseService creates a new Ent-backed license service.

func (*EntLicenseService) AssignSeat

func (s *EntLicenseService) AssignSeat(ctx context.Context, assignment *SeatAssignment) error

AssignSeat assigns a user to a license seat.

func (*EntLicenseService) Check

func (s *EntLicenseService) Check(ctx context.Context, listingID, orgID uuid.UUID) (bool, error)

Check checks if an organization has a valid license for a listing.

func (*EntLicenseService) CheckPrincipal

func (s *EntLicenseService) CheckPrincipal(ctx context.Context, listingID, principalID uuid.UUID) (bool, error)

CheckPrincipal checks if a principal has access via license.

func (*EntLicenseService) Get

func (s *EntLicenseService) Get(ctx context.Context, id uuid.UUID) (*License, error)

Get retrieves a license by ID.

func (*EntLicenseService) GetByListingAndOrg

func (s *EntLicenseService) GetByListingAndOrg(ctx context.Context, listingID, orgID uuid.UUID) (*License, error)

GetByListingAndOrg retrieves a license for a specific listing and organization.

func (*EntLicenseService) GetByStripeSubscriptionID added in v0.14.0

func (s *EntLicenseService) GetByStripeSubscriptionID(ctx context.Context, subscriptionID string) (*License, error)

GetByStripeSubscriptionID retrieves the license a Stripe subscription pays for.

func (*EntLicenseService) Grant

func (s *EntLicenseService) Grant(ctx context.Context, l *License) error

Grant creates a new license for an organization.

func (*EntLicenseService) List

func (s *EntLicenseService) List(ctx context.Context, orgID uuid.UUID) ([]*License, error)

List retrieves licenses for an organization.

func (*EntLicenseService) ListByListing

func (s *EntLicenseService) ListByListing(ctx context.Context, listingID uuid.UUID) ([]*License, error)

ListByListing retrieves all licenses for a listing.

func (*EntLicenseService) ListSeatAssignments

func (s *EntLicenseService) ListSeatAssignments(ctx context.Context, licenseID uuid.UUID) ([]*SeatAssignment, error)

ListSeatAssignments retrieves all seat assignments for a license.

func (*EntLicenseService) Revoke

func (s *EntLicenseService) Revoke(ctx context.Context, id uuid.UUID) error

Revoke revokes a license, removing access.

func (*EntLicenseService) UnassignSeat

func (s *EntLicenseService) UnassignSeat(ctx context.Context, licenseID, principalID uuid.UUID) error

UnassignSeat removes a user from a license seat.

func (*EntLicenseService) Update

func (s *EntLicenseService) Update(ctx context.Context, l *License) error

Update updates a license's details.

type EntListingService

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

EntListingService is an Ent-backed implementation of ListingService.

func NewEntListingService

func NewEntListingService(client *ent.Client, authzSync AuthzSyncer) *EntListingService

NewEntListingService creates a new Ent-backed listing service.

func (*EntListingService) Archive

func (s *EntListingService) Archive(ctx context.Context, id uuid.UUID) error

Archive archives a listing, removing it from the marketplace.

func (*EntListingService) Create

func (s *EntListingService) Create(ctx context.Context, l *Listing) error

Create creates a new listing in draft status.

func (*EntListingService) Delete

func (s *EntListingService) Delete(ctx context.Context, id uuid.UUID) error

Delete removes a listing (must be draft or archived).

func (*EntListingService) Get

func (s *EntListingService) Get(ctx context.Context, id uuid.UUID) (*Listing, error)

Get retrieves a listing by ID.

func (*EntListingService) GetByProduct

func (s *EntListingService) GetByProduct(ctx context.Context, productType string, productID uuid.UUID) (*Listing, error)

GetByProduct retrieves a listing by product type and ID.

func (*EntListingService) List

List retrieves listings with optional filters.

func (*EntListingService) ListByCreator

func (s *EntListingService) ListByCreator(ctx context.Context, creatorOrgID uuid.UUID) ([]*Listing, error)

ListByCreator retrieves all listings for a creator organization.

func (*EntListingService) Publish

func (s *EntListingService) Publish(ctx context.Context, id uuid.UUID) error

Publish publishes a listing to the marketplace.

func (*EntListingService) Update

func (s *EntListingService) Update(ctx context.Context, l *Listing) error

Update updates a listing's details.

type EntService

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

EntService is the Ent-backed implementation of the marketplace Service interface.

func NewEntService

func NewEntService(client *ent.Client, authzSync AuthzSyncer) *EntService

NewEntService creates a new Ent-backed marketplace service. The authzSync parameter is optional and can be nil if authorization sync is not needed.

func (*EntService) Checkout

func (s *EntService) Checkout() CheckoutService

Checkout returns the checkout service.

func (*EntService) Licenses

func (s *EntService) Licenses() LicenseService

Licenses returns the license service.

func (*EntService) Listings

func (s *EntService) Listings() ListingService

Listings returns the listing service.

func (*EntService) Subscriptions

func (s *EntService) Subscriptions() SubscriptionService

Subscriptions returns the subscription service.

func (*EntService) WithCheckout

func (s *EntService) WithCheckout(checkout CheckoutService) *EntService

WithCheckout sets the checkout service.

type EntSubscriptionService

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

EntSubscriptionService is an Ent-backed implementation of SubscriptionService.

func NewEntSubscriptionService

func NewEntSubscriptionService(client *ent.Client) *EntSubscriptionService

NewEntSubscriptionService creates a new Ent-backed subscription service.

func (*EntSubscriptionService) Cancel

func (s *EntSubscriptionService) Cancel(ctx context.Context, id uuid.UUID) error

Cancel cancels a subscription at period end.

func (*EntSubscriptionService) CancelImmediately

func (s *EntSubscriptionService) CancelImmediately(ctx context.Context, id uuid.UUID) error

CancelImmediately cancels a subscription immediately.

func (*EntSubscriptionService) ChangePlan

func (s *EntSubscriptionService) ChangePlan(ctx context.Context, id uuid.UUID, newPlan string) error

ChangePlan changes the subscription plan.

func (*EntSubscriptionService) Create

Create creates a new subscription.

func (*EntSubscriptionService) Get

Get retrieves a subscription by ID.

func (*EntSubscriptionService) GetByOrg

func (s *EntSubscriptionService) GetByOrg(ctx context.Context, orgID uuid.UUID) (*Subscription, error)

GetByOrg retrieves the organization-wide platform subscription of an organization, the one with no listing. Use ListByOrg for the per-listing subscriptions.

func (*EntSubscriptionService) GetPlanTier

func (s *EntSubscriptionService) GetPlanTier(ctx context.Context, orgID uuid.UUID) (string, error)

GetPlanTier returns the current plan tier for an organization.

func (*EntSubscriptionService) IsActive

func (s *EntSubscriptionService) IsActive(ctx context.Context, orgID uuid.UUID) (bool, error)

IsActive checks if an organization has an active subscription.

func (*EntSubscriptionService) ListByOrg added in v0.14.0

func (s *EntSubscriptionService) ListByOrg(ctx context.Context, orgID uuid.UUID) ([]*Subscription, error)

ListByOrg lists every subscription of an organization: the platform subscription and those for individual listings.

func (*EntSubscriptionService) Reactivate

func (s *EntSubscriptionService) Reactivate(ctx context.Context, id uuid.UUID) error

Reactivate reactivates a canceled subscription.

func (*EntSubscriptionService) Update

Update updates a subscription's details.

type EntitlementOption added in v0.14.0

type EntitlementOption func(*entitlementConfig)

EntitlementOption configures EntitlementSource.

func WithDefaultEntitlements added in v0.14.0

func WithDefaultEntitlements(applications ...string) EntitlementOption

WithDefaultEntitlements declares applications every organization is entitled to, with unlimited seats: applications that are free for everyone, or a deployment that does not sell access through the marketplace. It is the deployment's marketplace.default_entitlements.

type GetLicenseInput

type GetLicenseInput struct {
	ID string `path:"id" format:"uuid"`
}

GetLicenseInput is the request for getting a license.

type GetLicenseOutput

type GetLicenseOutput struct {
	Body *LicenseResponse
}

GetLicenseOutput is the response for getting a license.

type GetListingInput

type GetListingInput struct {
	ID string `path:"id" format:"uuid"`
}

GetListingInput is the request for getting a listing.

type GetListingOutput

type GetListingOutput struct {
	Body *ListingResponse
}

GetListingOutput is the response for getting a listing.

type License

type License struct {
	// ID is the unique identifier for this license.
	ID uuid.UUID `json:"id"`

	// ListingID references the marketplace listing.
	ListingID uuid.UUID `json:"listingId"`

	// OrganizationID is the organization that holds this license.
	OrganizationID uuid.UUID `json:"organizationId"`

	// LicenseType defines the scope of access.
	LicenseType LicenseType `json:"licenseType"`

	// Seats is the number of allowed users (nil for unlimited).
	Seats *int `json:"seats,omitempty"`

	// UsedSeats is the current number of assigned users.
	UsedSeats int `json:"usedSeats"`

	// ValidFrom is when the license becomes active.
	ValidFrom time.Time `json:"validFrom"`

	// ValidUntil is when the license expires (nil for perpetual).
	ValidUntil *time.Time `json:"validUntil,omitempty"`

	// StripeSubscriptionID is the Stripe subscription (for recurring).
	StripeSubscriptionID *string `json:"stripeSubscriptionId,omitempty"`

	// PurchasedBy is the principal who purchased this license.
	PurchasedBy uuid.UUID `json:"purchasedBy"`

	// CreatedAt is when the license was created.
	CreatedAt time.Time `json:"createdAt"`

	// UpdatedAt is when the license was last modified.
	UpdatedAt time.Time `json:"updatedAt"`
}

License represents an entitlement to use a product.

func (*License) HasAvailableSeats

func (l *License) HasAvailableSeats() bool

HasAvailableSeats returns true if seats are available for assignment.

func (*License) IsValid

func (l *License) IsValid() bool

IsValid returns true if the license is currently valid.

func (*License) SeatsRemaining

func (l *License) SeatsRemaining() int

SeatsRemaining returns the number of seats available. Returns -1 for unlimited licenses.

type LicenseResponse

type LicenseResponse struct {
	ID                   string     `json:"id"`
	ListingID            string     `json:"listing_id"`
	OrganizationID       string     `json:"organization_id"`
	LicenseType          string     `json:"license_type"`
	Seats                *int       `json:"seats,omitempty"`
	UsedSeats            int        `json:"used_seats"`
	ValidFrom            time.Time  `json:"valid_from"`
	ValidUntil           *time.Time `json:"valid_until,omitempty"`
	StripeSubscriptionID *string    `json:"stripe_subscription_id,omitempty"`
	PurchasedBy          string     `json:"purchased_by"`
	CreatedAt            time.Time  `json:"created_at"`
	UpdatedAt            time.Time  `json:"updated_at"`
}

LicenseResponse is the API representation of a license.

type LicenseService

type LicenseService interface {
	// Grant creates a new license for an organization.
	Grant(ctx context.Context, license *License) error

	// Get retrieves a license by ID.
	Get(ctx context.Context, id uuid.UUID) (*License, error)

	// GetByListingAndOrg retrieves a license for a specific listing and organization.
	GetByListingAndOrg(ctx context.Context, listingID, orgID uuid.UUID) (*License, error)

	// GetByStripeSubscriptionID retrieves the license a Stripe
	// subscription pays for, or ErrLicenseNotFound.
	GetByStripeSubscriptionID(ctx context.Context, subscriptionID string) (*License, error)

	// Update updates a license's details.
	Update(ctx context.Context, license *License) error

	// Revoke revokes a license, removing access.
	Revoke(ctx context.Context, id uuid.UUID) error

	// Check checks if an organization has a valid license for a listing.
	Check(ctx context.Context, listingID, orgID uuid.UUID) (bool, error)

	// CheckPrincipal checks if a principal has access via license.
	CheckPrincipal(ctx context.Context, listingID, principalID uuid.UUID) (bool, error)

	// List retrieves licenses for an organization.
	List(ctx context.Context, orgID uuid.UUID) ([]*License, error)

	// ListByListing retrieves all licenses for a listing.
	ListByListing(ctx context.Context, listingID uuid.UUID) ([]*License, error)

	// AssignSeat assigns a user to a license seat.
	AssignSeat(ctx context.Context, assignment *SeatAssignment) error

	// UnassignSeat removes a user from a license seat.
	UnassignSeat(ctx context.Context, licenseID, principalID uuid.UUID) error

	// ListSeatAssignments retrieves all seat assignments for a license.
	ListSeatAssignments(ctx context.Context, licenseID uuid.UUID) ([]*SeatAssignment, error)
}

LicenseService provides operations for licenses and entitlements.

type LicenseType

type LicenseType string

LicenseType defines the scope of a license.

const (
	// LicenseSeatBased grants access to a specific number of users.
	LicenseSeatBased LicenseType = "seat_based"

	// LicenseTeam grants access to all members of a team/group.
	LicenseTeam LicenseType = "team"

	// LicenseUnlimited grants unlimited access within an organization.
	LicenseUnlimited LicenseType = "unlimited"
)

func LicenseTypes

func LicenseTypes() []LicenseType

LicenseTypes returns all valid license types.

type ListLicensesInput

type ListLicensesInput struct {
	OrganizationID string `query:"organization_id" required:"true" format:"uuid"`
}

ListLicensesInput is the request for listing licenses.

type ListLicensesOutput

type ListLicensesOutput struct {
	Body struct {
		Licenses []*LicenseResponse `json:"licenses"`
	}
}

ListLicensesOutput is the response for listing licenses.

type ListListingsInput

type ListListingsInput struct {
	// Huma does not support pointer parameters; an empty value means
	// the filter is not applied.
	Status       string `query:"status" enum:"draft,pending_review,published,archived"`
	ProductType  string `query:"product_type"`
	CreatorOrgID string `query:"creator_org_id" format:"uuid"`
	Published    bool   `query:"published"`
	Limit        int    `query:"limit" default:"20" minimum:"1" maximum:"100"`
	Offset       int    `query:"offset" default:"0" minimum:"0"`
}

ListListingsInput is the request for listing listings.

type ListListingsOptions

type ListListingsOptions struct {
	// Status filters by listing status.
	Status *ListingStatus

	// ProductType filters by product type.
	ProductType *string

	// CreatorOrgID filters by creator organization.
	CreatorOrgID *uuid.UUID

	// PublishedOnly returns only published listings.
	PublishedOnly bool

	// Limit is the maximum number of results.
	Limit int

	// Offset is the pagination offset.
	Offset int

	// OrderBy specifies the sort order.
	OrderBy string
}

ListListingsOptions configures listing queries.

type ListListingsOutput

type ListListingsOutput struct {
	Body struct {
		Listings []*ListingResponse `json:"listings"`
		Total    int                `json:"total"`
	}
}

ListListingsOutput is the response for listing listings.

type ListSeatsInput

type ListSeatsInput struct {
	LicenseID string `path:"license_id" format:"uuid"`
}

ListSeatsInput is the request for listing seat assignments.

type ListSeatsOutput

type ListSeatsOutput struct {
	Body struct {
		Seats          []*SeatAssignmentResponse `json:"seats"`
		TotalSeats     *int                      `json:"total_seats,omitempty"`
		UsedSeats      int                       `json:"used_seats"`
		AvailableSeats int                       `json:"available_seats"`
	}
}

ListSeatsOutput is the response for listing seat assignments.

type Listing

type Listing struct {
	// ID is the unique identifier for this listing.
	ID uuid.UUID `json:"id"`

	// CreatorOrgID is the organization that created this listing.
	CreatorOrgID uuid.UUID `json:"creatorOrgId"`

	// OwnerID is the principal who owns this listing.
	OwnerID uuid.UUID `json:"ownerId"`

	// ProductType identifies the type of product (app-specific).
	// Examples: "course", "dashboard_template", "data_connector"
	ProductType string `json:"productType"`

	// ProductID references the actual product in the app's domain.
	ProductID uuid.UUID `json:"productId"`

	// Application is the registered application identifier (an
	// authz.AppVocabulary application) that an active subscription or valid
	// license on this listing entitles its organization to. Empty when the
	// listing does not grant an application.
	Application string `json:"application,omitempty"`

	// Title is the display name for the listing.
	Title string `json:"title"`

	// Description is the detailed description of the product.
	Description string `json:"description,omitempty"`

	// PricingModel defines how the product is priced.
	PricingModel PricingModel `json:"pricingModel"`

	// PriceCents is the price in cents (0 for free).
	PriceCents int64 `json:"priceCents"`

	// Currency is the ISO 4217 currency code.
	Currency string `json:"currency"`

	// Status is the publication state.
	Status ListingStatus `json:"status"`

	// Metadata contains app-specific data.
	Metadata map[string]any `json:"metadata,omitempty"`

	// CreatedAt is when the listing was created.
	CreatedAt time.Time `json:"createdAt"`

	// UpdatedAt is when the listing was last modified.
	UpdatedAt time.Time `json:"updatedAt"`

	// PublishedAt is when the listing was published (nil if not published).
	PublishedAt *time.Time `json:"publishedAt,omitempty"`
}

Listing represents a product available on the marketplace.

func (*Listing) IsFree

func (l *Listing) IsFree() bool

IsFree returns true if the listing has no cost.

func (*Listing) IsPublished

func (l *Listing) IsPublished() bool

IsPublished returns true if the listing is live on the marketplace.

type ListingResponse

type ListingResponse struct {
	ID           string         `json:"id"`
	CreatorOrgID string         `json:"creator_org_id"`
	OwnerID      string         `json:"owner_id"`
	ProductType  string         `json:"product_type"`
	ProductID    *string        `json:"product_id,omitempty"`
	Application  string         `json:"application,omitempty"`
	Title        string         `json:"title"`
	Description  string         `json:"description,omitempty"`
	PricingModel string         `json:"pricing_model"`
	PriceCents   int64          `json:"price_cents"`
	Currency     string         `json:"currency"`
	Status       string         `json:"status"`
	Metadata     map[string]any `json:"metadata,omitempty"`
	CreatedAt    time.Time      `json:"created_at"`
	UpdatedAt    time.Time      `json:"updated_at"`
	PublishedAt  *time.Time     `json:"published_at,omitempty"`
}

ListingResponse is the API representation of a listing.

type ListingService

type ListingService interface {
	// Create creates a new listing in draft status.
	Create(ctx context.Context, listing *Listing) error

	// Get retrieves a listing by ID.
	Get(ctx context.Context, id uuid.UUID) (*Listing, error)

	// GetByProduct retrieves a listing by product type and ID.
	GetByProduct(ctx context.Context, productType string, productID uuid.UUID) (*Listing, error)

	// Update updates a listing's details.
	Update(ctx context.Context, listing *Listing) error

	// Delete removes a listing (must be draft or archived).
	Delete(ctx context.Context, id uuid.UUID) error

	// Publish publishes a listing to the marketplace.
	Publish(ctx context.Context, id uuid.UUID) error

	// Archive archives a listing, removing it from the marketplace.
	Archive(ctx context.Context, id uuid.UUID) error

	// List retrieves listings with optional filters.
	List(ctx context.Context, opts ListListingsOptions) ([]*Listing, error)

	// ListByCreator retrieves all listings for a creator organization.
	ListByCreator(ctx context.Context, creatorOrgID uuid.UUID) ([]*Listing, error)
}

ListingService provides operations for marketplace listings.

type ListingStatus

type ListingStatus string

ListingStatus represents the publication state of a listing.

const (
	// ListingStatusDraft indicates the listing is being prepared.
	ListingStatusDraft ListingStatus = "draft"

	// ListingStatusPendingReview indicates the listing is awaiting approval.
	ListingStatusPendingReview ListingStatus = "pending_review"

	// ListingStatusPublished indicates the listing is live on the marketplace.
	ListingStatusPublished ListingStatus = "published"

	// ListingStatusArchived indicates the listing has been retired.
	ListingStatusArchived ListingStatus = "archived"
)

func ListingStatuses

func ListingStatuses() []ListingStatus

ListingStatuses returns all valid listing statuses.

type PricingModel

type PricingModel string

PricingModel defines how a product is priced.

const (
	// PricingFree indicates no charge (may require attribution).
	PricingFree PricingModel = "free"

	// PricingOneTime indicates a single purchase for perpetual access.
	PricingOneTime PricingModel = "one_time"

	// PricingSubscription indicates recurring monthly/annual billing.
	PricingSubscription PricingModel = "subscription"

	// PricingPerSeat indicates per-user pricing within an organization.
	PricingPerSeat PricingModel = "per_seat"
)

func PricingModels

func PricingModels() []PricingModel

PricingModels returns all valid pricing models.

type PublishListingInput

type PublishListingInput struct {
	ID string `path:"id" format:"uuid"`
}

PublishListingInput is the request for publishing a listing.

type PublishListingOutput

type PublishListingOutput struct {
	Body *ListingResponse
}

PublishListingOutput is the response for publishing a listing.

type PurchaseAuthorizer added in v0.14.0

type PurchaseAuthorizer interface {
	CanPurchase(ctx context.Context, principalID, orgID uuid.UUID) (bool, error)
}

PurchaseAuthorizer decides whether a principal may buy on behalf of an organization.

func OrgRolePurchaseAuthorizer added in v0.14.0

func OrgRolePurchaseAuthorizer(orgs authz.OrgAuthorizer, roles ...string) PurchaseAuthorizer

OrgRolePurchaseAuthorizer allows purchases by principals holding one of roles in the organization, DefaultPurchaseRoles when none are given. It uses the organization role, which both authorization providers report the same way.

type PurchaseAuthorizerFunc added in v0.14.0

type PurchaseAuthorizerFunc func(ctx context.Context, principalID, orgID uuid.UUID) (bool, error)

PurchaseAuthorizerFunc adapts a function to PurchaseAuthorizer.

func (PurchaseAuthorizerFunc) CanPurchase added in v0.14.0

func (f PurchaseAuthorizerFunc) CanPurchase(ctx context.Context, principalID, orgID uuid.UUID) (bool, error)

CanPurchase implements PurchaseAuthorizer.

type PurchaserChecker added in v0.14.0

type PurchaserChecker interface {
	IsManaged(ctx context.Context, principalID uuid.UUID) (bool, error)
}

PurchaserChecker reports whether a principal is managed: a human without an email whose managers control the account. Managed principals cannot purchase. identity/managedprincipal.Service implements it.

type RevenueShare

type RevenueShare struct {
	// CreatorPercent is the percentage going to the creator (0-100).
	CreatorPercent int `json:"creatorPercent"`

	// PlatformPercent is the percentage going to the platform (0-100).
	PlatformPercent int `json:"platformPercent"`
}

RevenueShare defines the revenue split for marketplace sales.

func DefaultRevenueShare

func DefaultRevenueShare() RevenueShare

DefaultRevenueShare returns the default revenue split (70/30).

func (RevenueShare) Validate

func (r RevenueShare) Validate() error

Validate checks if the revenue share adds up to 100%.

type SeatAssignment

type SeatAssignment struct {
	// ID is the unique identifier for this assignment.
	ID uuid.UUID `json:"id"`

	// LicenseID references the license.
	LicenseID uuid.UUID `json:"licenseId"`

	// PrincipalID is the user assigned to this seat.
	PrincipalID uuid.UUID `json:"principalId"`

	// AssignedBy is who made this assignment.
	AssignedBy uuid.UUID `json:"assignedBy"`

	// AssignedAt is when the seat was assigned.
	AssignedAt time.Time `json:"assignedAt"`
}

SeatAssignment represents a user's assignment to a license seat.

type SeatAssignmentResponse

type SeatAssignmentResponse struct {
	ID          string    `json:"id"`
	LicenseID   string    `json:"license_id"`
	PrincipalID string    `json:"principal_id"`
	AssignedBy  string    `json:"assigned_by"`
	AssignedAt  time.Time `json:"assigned_at"`
}

SeatAssignmentResponse is the API representation of a seat assignment.

type Service

type Service interface {
	Listings() ListingService
	Licenses() LicenseService
	Subscriptions() SubscriptionService
	Checkout() CheckoutService
}

Service combines all marketplace services.

type SpiceDBSyncer

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

SpiceDBSyncer implements AuthzSyncer using SpiceDB.

func NewSpiceDBSyncer

func NewSpiceDBSyncer(client *spicedb.Client) *SpiceDBSyncer

NewSpiceDBSyncer creates a new SpiceDB syncer for marketplace entities.

func (*SpiceDBSyncer) SyncLicense

func (s *SpiceDBSyncer) SyncLicense(ctx context.Context, license *License) error

SyncLicense syncs a license grant to SpiceDB. Creates relationships:

  • listing:listingId -> licensed_org -> organization:orgId
  • license:id -> listing -> listing:listingId
  • license:id -> organization -> organization:orgId
  • license:id -> purchased_by -> principal:purchasedBy

func (*SpiceDBSyncer) SyncLicenseRevocation

func (s *SpiceDBSyncer) SyncLicenseRevocation(ctx context.Context, license *License) error

SyncLicenseRevocation removes a license from SpiceDB. Removes relationships:

  • listing:listingId -> licensed_org -> organization:orgId

func (*SpiceDBSyncer) SyncListing

func (s *SpiceDBSyncer) SyncListing(ctx context.Context, listing *Listing) error

SyncListing syncs a listing to SpiceDB. Creates relationships:

  • listing:id -> creator_org -> creator_org:creatorOrgId
  • listing:id -> owner -> principal:ownerId

func (*SpiceDBSyncer) SyncSeatAssignment

func (s *SpiceDBSyncer) SyncSeatAssignment(ctx context.Context, assignment *SeatAssignment) error

SyncSeatAssignment syncs a seat assignment to SpiceDB. Creates relationship:

  • license:licenseId -> seat_holder -> principal:principalId

func (*SpiceDBSyncer) SyncSeatUnassignment

func (s *SpiceDBSyncer) SyncSeatUnassignment(ctx context.Context, licenseID, principalID uuid.UUID) error

SyncSeatUnassignment removes a seat assignment from SpiceDB. Removes relationship:

  • license:licenseId -> seat_holder -> principal:principalId

func (*SpiceDBSyncer) SyncSubscription

func (s *SpiceDBSyncer) SyncSubscription(ctx context.Context, sub *Subscription) error

SyncSubscription syncs a subscription to SpiceDB. Creates relationships:

  • subscription:id -> organization -> organization:orgId

type Subscription

type Subscription struct {
	// ID is the unique identifier for this subscription.
	ID uuid.UUID `json:"id"`

	// OrganizationID is the subscribing organization.
	OrganizationID uuid.UUID `json:"organizationId"`

	// ListingID is the listing this subscription is for. It is nil for the
	// organization-wide platform subscription, of which an organization
	// has at most one; it may hold one subscription per listing.
	ListingID *uuid.UUID `json:"listingId,omitempty"`

	// PlanTier is the subscription plan name.
	PlanTier string `json:"planTier"`

	// Status is the subscription state.
	Status SubscriptionStatus `json:"status"`

	// CurrentPeriodStart is when the current billing period began.
	CurrentPeriodStart time.Time `json:"currentPeriodStart"`

	// CurrentPeriodEnd is when the current billing period ends.
	CurrentPeriodEnd time.Time `json:"currentPeriodEnd"`

	// StripeSubscriptionID is the Stripe subscription ID.
	StripeSubscriptionID string `json:"stripeSubscriptionId"`

	// StripeCustomerID is the Stripe customer ID.
	StripeCustomerID string `json:"stripeCustomerId"`

	// CancelAtPeriodEnd indicates if subscription will cancel at period end.
	CancelAtPeriodEnd bool `json:"cancelAtPeriodEnd"`

	// CreatedAt is when the subscription was created.
	CreatedAt time.Time `json:"createdAt"`

	// UpdatedAt is when the subscription was last modified.
	UpdatedAt time.Time `json:"updatedAt"`
}

Subscription represents a platform-level subscription for an organization.

func (*Subscription) DaysRemaining

func (s *Subscription) DaysRemaining() int

DaysRemaining returns the days until the current period ends.

func (*Subscription) IsActive

func (s *Subscription) IsActive() bool

IsActive returns true if the subscription is currently active.

func (*Subscription) IsInTrial

func (s *Subscription) IsInTrial() bool

IsInTrial returns true if the subscription is in trial period.

type SubscriptionLister added in v0.14.0

type SubscriptionLister interface {
	ListByOrg(ctx context.Context, orgID uuid.UUID) ([]*Subscription, error)
}

SubscriptionLister is implemented by a SubscriptionService that can list every subscription of an organization, the platform subscription and those for individual listings. It is a separate interface so existing SubscriptionService implementers keep compiling; without it, only the platform subscription from GetByOrg is considered.

type SubscriptionService

type SubscriptionService interface {
	// Create creates a new subscription.
	Create(ctx context.Context, sub *Subscription) error

	// Get retrieves a subscription by ID.
	Get(ctx context.Context, id uuid.UUID) (*Subscription, error)

	// GetByOrg retrieves the subscription for an organization.
	GetByOrg(ctx context.Context, orgID uuid.UUID) (*Subscription, error)

	// Update updates a subscription's details.
	Update(ctx context.Context, sub *Subscription) error

	// Cancel cancels a subscription at period end.
	Cancel(ctx context.Context, id uuid.UUID) error

	// CancelImmediately cancels a subscription immediately.
	CancelImmediately(ctx context.Context, id uuid.UUID) error

	// Reactivate reactivates a canceled subscription.
	Reactivate(ctx context.Context, id uuid.UUID) error

	// ChangePlan changes the subscription plan.
	ChangePlan(ctx context.Context, id uuid.UUID, newPlan string) error

	// IsActive checks if an organization has an active subscription.
	IsActive(ctx context.Context, orgID uuid.UUID) (bool, error)

	// GetPlanTier returns the current plan tier for an organization.
	GetPlanTier(ctx context.Context, orgID uuid.UUID) (string, error)
}

SubscriptionService provides operations for platform subscriptions.

type SubscriptionStatus

type SubscriptionStatus string

SubscriptionStatus represents the state of a platform subscription.

const (
	// SubscriptionStatusActive indicates the subscription is current.
	SubscriptionStatusActive SubscriptionStatus = "active"

	// SubscriptionStatusTrialing indicates the subscription is in trial period.
	SubscriptionStatusTrialing SubscriptionStatus = "trialing"

	// SubscriptionStatusPastDue indicates payment has failed.
	SubscriptionStatusPastDue SubscriptionStatus = "past_due"

	// SubscriptionStatusCanceled indicates the subscription has been canceled.
	SubscriptionStatusCanceled SubscriptionStatus = "canceled"

	// SubscriptionStatusUnpaid indicates multiple payment failures.
	SubscriptionStatusUnpaid SubscriptionStatus = "unpaid"
)

func SubscriptionStatuses

func SubscriptionStatuses() []SubscriptionStatus

SubscriptionStatuses returns all valid subscription statuses.

type UnassignSeatInput

type UnassignSeatInput struct {
	LicenseID   string `path:"license_id" format:"uuid"`
	PrincipalID string `path:"principal_id" format:"uuid"`
}

UnassignSeatInput is the request for unassigning a seat.

type UpdateListingInput

type UpdateListingInput struct {
	ID   string `path:"id" format:"uuid"`
	Body struct {
		Title        *string        `json:"title,omitempty" maxLength:"200"`
		Description  *string        `json:"description,omitempty" maxLength:"5000"`
		PricingModel *string        `json:"pricing_model,omitempty" enum:"free,one_time,subscription,per_seat"`
		PriceCents   *int64         `json:"price_cents,omitempty" minimum:"0"`
		Currency     *string        `json:"currency,omitempty" minLength:"3" maxLength:"3"`
		ProductID    *string        `json:"product_id,omitempty" format:"uuid"`
		Metadata     map[string]any `json:"metadata,omitempty"`
	}
}

UpdateListingInput is the request for updating a listing.

type UpdateListingOutput

type UpdateListingOutput struct {
	Body *ListingResponse
}

UpdateListingOutput is the response for updating a listing.

type WebhookInput

type WebhookInput struct {
	StripeSignature string `header:"Stripe-Signature"`
	RawBody         []byte
}

WebhookInput is the request for Stripe webhooks.

Directories

Path Synopsis
Package stripe provides Stripe integration for the marketplace.
Package stripe provides Stripe integration for the marketplace.

Jump to

Keyboard shortcuts

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