Skip to content

Membership products (Admin)

The admin screen Packages manages rows in the membership_products table with audience platform_vendor: quota-style entitlements for vendor accounts (stores, products, staff, devices, customers, orders), not store customer loyalty programs.


  • /dashboard/membership-products — list, search, status filter, pagination, create / edit / delete.
  • /dashboard/membership-subscriptions — read-only subscription list for platform vendors only (platform_vendor) for support/audit workflows.
  • /dashboard/membership-invoices — invoice-style view for vendor package purchases (derived from platform_vendor subscriptions with paid/unpaid status).
  • /dashboard/membership-invoices/:id — invoice detail page with Download invoice action (text export).
  • benefits_json is edited via structured quota fields in the modal: max_stores, max_products, max_pos_devices, max_staff_seats, max_customers, max_orders. Saving writes those rows only (legacy module_unlock / feature_flag rows are not re-emitted). The API validates with parseAndValidateMembershipBenefitsJson (membership-benefits-json helpers).
  • Code auto-generation: when creating a plan and code is blank, backend auto-generates a unique code.
  • Delete guard: deleting a plan is blocked when it is linked to vendors (derived from membership_subscriptions.membership_product_id).
  • Detail screen: shows linked vendors (search + pagination), plan details, and Member perks (benefits_json).
Section titled “Optional: link to this doc from the admin UI”

Set VITE_PUBLIC_DOCS_BASE_URL in apps/admin-web (no trailing slash) to the public Starlight origin, e.g. https://docs.example.com. The membership plan modal then shows an Open documentation link to /guides/admin/membership-products/.


  • AdminJwtAuthGuard on GET/POST/PATCH/DELETE /admin/membership-products.
  • Any signed-in admin with a valid JWT can call these routes (no extra module guard on the controller). Zone or role limits for “who may edit SaaS plans” are a product decision — if tightened later, document the new guard here.

Implemented in apps/admin-web/src/services/admin-membership-products.api.ts (base path is the usual API prefix, e.g. /api/v1, via shared Axios config):

MethodPathPurpose
GET/admin/membership-productsList (page, limit, search, status)
GET/admin/membership-products/:idDetail
POST/admin/membership-productsCreate
PATCH/admin/membership-products/:idUpdate
DELETE/admin/membership-products/:idDelete
GET/admin/membership-subscriptionsList vendor subscriptions / invoices (page, limit, search, status, payment_status, vendor_id, user_id)
GET/admin/membership-subscriptions/:idMembership invoice detail by subscription id

Vendor self-serve checkout flow (used by Vendor Web platform-membership screen):

MethodPathPurpose
GET/vendor/membership-products/platform-packages/listList active platform_vendor catalog packages
POST/vendor/membership-products/platform-packages/:id/purchaseCreate/reuse pending invoice (past_due) for package
GET/vendor/membership-products/platform-invoicesList package-purchase invoices for current vendor
GET/vendor/membership-products/platform-invoices/:idInvoice detail for current vendor
POST/vendor/membership-products/platform-invoices/:id/payMark invoice as paid and activate subscription

New rows created from this surface are stored with audience = platform_vendor and vendor_id / store_id unset at the catalog level (SaaS plan definition).


Difference from vendor “customer membership”

Section titled “Difference from vendor “customer membership””
SurfaceAudienceWho it is for
Admin — this pageplatform_vendorVendor accounts (SaaS)
VendorMembership products (Vendor)vendor_customerEnd customers of a store