@forgeco/ext-subscriptions
v0.3.2
Published
Forge extension (SUBSCRIPTIONS): sells a recurring order. The shopper picks a delivery rhythm on the product page (the app's declared `sub_plan` cart-line custom field) and may change it in the checkout summary; the app keeps a CONTRACT of its own — custo
Readme
@forgeco/ext-subscriptions
Recurring purchases as contracts. A shopper picks a delivery rhythm on the product page; each cycle fabricates a normal order — never a recurring one — carrying the mark of the subscription and the cycle it came from.
What the app is, in three faces
- The engine. A contract of the app's own — customer, store, items, rhythm, next delivery, cycle counter — born from the KERNEL's frozen order line, plus the scheduled run and the idempotency on (contract, cycle).
- The merchant's screen. A whole admin page the app owns (
APP-ADMIN-PAGE): every contract with a status filter, one contract's cycle history with a link to the order each cycle made, and the action that runs a contract's next cycle. - The shopper's screens — the half this package publishes: a "buy once × subscribe" picker under the buybox, and a per-line rhythm switcher inside the checkout summary.
The front half — what this package ships to a customer's storefront
import { PlanPickerBlock } from '@forgeco/ext-subscriptions/block/plan-picker';
import { CartPlansBlock } from '@forgeco/ext-subscriptions/block/cart-plans';Both stand on the kernel and invent nothing. The shopper's choice rides the cart line's own declared custom
field (sub_plan), which buys two properties no code here implements:
cart.add_lineaggregates by sku + custom fields, so the same coffee bought once and subscribed monthly are two lines by construction — a mixed cart needs no special case;checkout.place_ordersnapshots a line's custom fields into the order line, so the order that is born already carries the rhythm, immutably — which is what the engine mints the contract from.
Changing a rhythm at the checkout is therefore a migration of the line (remove + add), never an edit: the line the shopper wants does not exist yet, and the one they have must stop existing.
What is subscribable — the merchant curates, one SKU at a time
Installing this app makes nothing subscribable. The rhythms are the STORE's (frequencies, the app
config), but the OFFER is the SKU's: PlanPickerBlock draws nothing unless the SKU the page is about carries
the mark sub_enabled = true in its own metadata bag. Absent, false, null, the string "true" — none
of them is a mark. Default false, so a live catalogue does not sprout a subscription control on every product
page the moment the app arrives.
The mark is on the SKU, not the product, because a SKU is what is bought: a 250 g bag on a monthly rhythm
and a 1 kg sack nobody wants monthly are two SKUs of one product. The picker follows the shopper's variant for
free — the product page's selector navigates to ?sku=<code> and the block reads the mark of the SKU that
?sku= names, in the same render that changes the price.
Marking is data, so it goes through the port like any other SKU data — which is also how a merchant marks three hundred of them:
catalog.sku.update { sku_id: "sku_…", metadata: { "sub_enabled": true } }Why the scope moved to @forgeco/*
This package was @forge/* and private while it carried only the admin face, and its own README said the
reason out loud: "nothing here is installable yet… when the engine lands, the app joins the composition list
and the scope moves with it." Both happened. The app is in the composition list and it now ships storefront
blocks, which are exactly the thing a customer who forks the vitrine has to be able to install. The
extension id (subscriptions) is runtime identity and never moved.
The admin half travels in the image and is deliberately absent from publishConfig.exports: an app's
admin screen is not something a storefront installs.
Next 15.2 or newer
The next peer range starts at 15.2.0, the same floor every front package in this set declares.
Install
The Forge packages are published as tarballs on the GitHub Release, so install them together — they resolve each other from the sibling files, not from a registry:
npm i ./forgeco-contracts-<version>.tgz ./forgeco-ext-*-<version>.tgz⚠️ One required line in next.config
This package ships its components as source (.tsx + CSS Modules), so your bundler compiles them along
with your own code — that is what keeps the styles, the tokens and the tree-shaking working. Next skips
node_modules unless you say otherwise:
// next.config.mjs
export default {
transpilePackages: ['@forgeco/ext-subscriptions'],
};Without it the build fails loudly (Module parse failed: Unexpected token) — it never degrades into an
unstyled page.
The blocks style themselves in semantic design tokens, never literal colours, so install
@forgeco/theme-storefront-vanilla (or provide the same token names from your own theme) and import its
tokens.css once in your root layout.
What it does NOT do
- No discount is applied from the subscriber percentage. The number is stored on the contract for a merchant
migrating in, and no surface in this package renders it as money off. A merchant who wants the subscribed line
discounted creates a promotion targeting the line's own custom field
sub_plan(target: { kind: custom_field, … }) — money said by the kernel's promotion engine, never by this app. Seedocs/concepts/promocao-por-campo-proprio.md. - It never invents a charge mandate. It stores no card. See the two cycle modes in
config.ts.
Licence
Apache-2.0. The Forge kernel itself is BUSL-1.1; what you install and compile against is permissive on purpose — your storefront code is yours.
