# Subscription-first commerce for physical goods, on code you own

The subscription layer in the open: renewals, dunning, payment recovery, self-service. Not a Shopify add-on. Not a billing API.

Development preview. npm 0.37.0.

## Subscription-first commerce for physical goods, on code you own

The subscription layer in the open: renewals, dunning, payment recovery, self-service. Not a Shopify add-on. Not a billing API.

- [Read the engine contract](https://github.com/openlup/openlup/blob/openlup-core-v0.37.0/packages/core/docs/SUBSCRIPTION_ENGINE.md)

- [View the source](https://github.com/openlup/openlup)

Shell

```sh
npm install --save-exact @openlup/core@0.37.0
```

Development preview. npm 0.37.0. Running on velipet.com, the author’s shop.

### One late payment

Figure: One late payment on a day scale from day 0 to day 39. Day 0 the cycle was scheduled, day 11 it was paid late, day 28 is one cadence after the schedule. Before: the next charge fell on day 28, only 17 days after the late payment. Now: it moves to day 39, 28 days after — the cadence the customer paid for.

Day 0 · Day 11 · Day 28 · Day 39

- **Before** — 17 days to the next charge

- **Now** — 28 days, the cadence the customer paid for

`next = max(next, scheduled + late_days + cadence)`

*Pinned by a pgTAP test of the late-payment shift.*

## Built and tested on

TypeScript · Node.js 24 · PostgreSQL 16 · Vercel · Supabase · OCI image

Providers sit behind ports. [Ownership and extension](#ownership)

## Shelf space is rented. Subscribers are not

- **Finite shelf** — A listing you rent, capped by what fits
- **No relationship** — The buyer is theirs, not yours
- **Direct compounds** — Subscription is where repeat value can accrue to you
- **Closed or generic** — Rent the subscription layer, or own a system that lacks it

> Subscriptions are the business, not an add-on. OpenLup keeps them in the open core, with no app fee and no revenue share.

## Categories

- **Replenishment** — The same products, delivered again on the customer’s cadence
- **Curated boxes** — A box assembled for each cycle, from your catalogue
- **Repeat order** — The customer’s own basket, ordered again when they want it

The core knows neither your brand nor your country.

## The subscription layer, in code you own

- **Subscription engine** — Cycles, renewals, pauses, skips, line swaps, add-ons
- **Dunning and recovery** — A retry ladder that terminates, and one-use recovery links
- **Customer self-service** — Pause, skip, swap a line, repair a card; off by default
- **Operator panel** — Order queue with an attention ladder, holds, replacements
- **Transactional messaging** — Outbox, consent, delivery reconciliation, health verdict
- **Working store from npm** — Install from npm, guest checkout, sandbox payments, renewals and recovery — In progress · M19a
- **Subscription care** — Pause and resume, reminders, escalation, win-back, order now, save offers — Planned · M19b1a
- **Payment recovery and account** — Recovery reminders and pages, saved addresses, billing profile, operator replacements — Planned · M19b1b
- **Subscription editor** — Classic editor with price preview; swap, mix, portion and size levers — Planned · M19b2

227 pgTAP files · 2 389 test files · 113 platform migrations · measured on npm 0.37.0

**Also in the tree**

A neutral store starter to begin from; kernels for bundles, inventory, pricing, promotions, shipping, risk and company identity; and example adapters for payments, fulfilment and e-mail.

## One system from first order to the next renewal

Figure: The full cycle, a loop. Happy path: checkout, mandate, fulfilment, delivery, next cycle, renewal, and renewal returns to fulfilment; while a delivery is unknown, renewals wait. When renewal fails: the dunning ladder at 24, 72 and 168 hours, then automatic retries stop while the subscription stays active, then the customer repairs the card or changes the address or cancels, and the same cycle is charged again. Transactional messaging runs under both paths.

- Checkout → Mandate → Fulfilment → Delivery → Next cycle → Renewal → Fulfilment

- Delivery → Delivery unknown · renewals wait

- Fails → Dunning ladder · 24 h · 72 h · 168 h → Retries stop → Customer repair · card · address · cancel → same cycle, charged again

- Transactional messaging runs under all of it

*Happy path above, failure and repair below, messaging under both.*

## The split

| Open core | Yours |
| --- | --- |
| Checkout to renewal, the subscription path | Brand look and identity |
| Subscriptions, dunning, mandates, recovery | Copy, tone, configurator logic |
| Messaging with schedule and observability | Taxes, carriers, fiscal rules |
| Customer account and self-service actions | ERP, WMS, CRM integrations |
| Operator panel | Pricing and promotion rules |
| Payments, fulfilment, invoicing behind ports | Hosting choice |
| Runtime, migrations, scheduler, storage | — |

*Nothing is withheld. Limits are documented.*

## What the engine promises

### Never paid

Subscriptions, dunning, mandates and recovery are open core and will not become paid tiers.

```TypeScript
export type SubscriptionEngineErrorCode =
  | "invalid_timestamp"
  | "invalid_template"
  | "inactive_subscription"
  | "edit_window_closed"
  | "line_not_found"
  | "duplicate_line"
  | "invalid_slide_target"
  | "invalid_transition"
  | "terminal_cycle"
  | "missing_payment_method"
  | "dunning_not_exhausted";
```

*Refusals are values. Nothing throws.*

### Never compressed

A late payment moves the next cycle later by the days it was late. Never earlier.

```TypeScript
const shiftedByDays = Math.max(0, Math.floor(
  (Math.min(paidAt.getTime(), recordedAt.getTime()) - Date.parse(input.cycle.scheduledAt)) / DAY_MS,
));
const nextCycleAt = new Date(Math.max(
  Date.parse(input.subscription.nextCycleAt) || 0,
  Date.parse(onTimeNextCycleAt) + shiftedByDays * DAY_MS,
)).toJSON();
```

*From subscriptionEnginePayment.ts, npm 0.37.0.*

### Terminates

Three rungs, then automatic retries stop. The subscription stays active and repairable.

```TypeScript
if (ladderTerminatedByClass(failureClass, cadence)) return null;
const hours = cadence.backoffHours[attemptNumber - 1];
// Covers both ends at once: an attempt below 1 and an attempt past the last
// ladder slot are equally "no next charge is due".
if (attemptNumber < 1 || hours === undefined) return null;
```

*From cycleHardening.ts, npm 0.37.0.*

### Delivered

Lifecycle e-mail goes through a durable outbox, and a dropped webhook is reconciled by polling.

### Tested

Failure paths are pinned by tests: 227 pgTAP files and 2 389 test files at npm 0.37.0.

## A late payment shifts the cycle. It never compresses it

The customer keeps the cadence they paid for, whatever the payment did.

- 28 days — the gap bought
- 17 days — what a day-11 recovery used to leave
- 28 days — what it leaves now

Measured 2026-08-26

Figure: Day scale. Day 0 the cycle was scheduled, day 11 it was paid late, day 28 is one cadence after the schedule. Before: the next charge fell on day 28, only 17 days after the late payment. Now: it moves to day 39, 28 days after.

Day 0 scheduled · Day 11 paid late · Day 28 cadence · Day 39 renewal

- **Before** — 17 days to the next charge

- **Now** — 28 days, the cadence the customer paid for

`next = max(next, scheduled + late_days + cadence)`

*Pinned by a pgTAP test of the late-payment shift.*

**Why the clamp is load-bearing**

The next cycle is the cycle’s own scheduled date plus its cadence plus the days the payment was late, and it never moves earlier. The clamp means a replayed payment webhook cannot pull a charge forward into a window the customer already paid for.

**A late parcel moves the next cycle too**

With delivery alignment on, the next cycle starts from the latest confirmed delivery plus the cadence, and renewals wait while a parcel’s outcome is unknown, so parcels never stack up. The mode is off by default.

## The retry ladder terminates. It does not cap

Figure: Retry ladder. Three rungs at 24 hours, 72 hours and 168 hours after the previous failure, each with an exit to recovered. After the third rung automatic retries stop; the subscription stays active and the case stays open for the customer to repair. The customer repairs the card or the address and the same cycle is charged again, or cancels and meets the save offers; the operator escalation and the save offers are planned in M19b1a.

- 24 h → recovered
- 72 h → recovered
- 168 h → recovered

then Retries stop

- Retries stop → Operator escalation (Planned · M19b1a)
- Retries stop → Customer repair: card · address · cancel → Same cycle, charged again
- Customer repair → cancel → Save offers (Planned · M19b1a)

*Nothing repeats its last slot forever.*

- **24 h · 72 h · 168 h** — Fixed in the managed SQL, injectable in the pure core
- **A class can end it early** — When the issuer refused the instrument, the ladder stops at once
- **Recovery is a token** — Single-use and owner-bound; redemption requires the owner’s session

## Transactional e-mail with an outbox and reconciliation

- **Outbox and e-mail ledger, each a state machine**
- **Marketing consent checked before a send**
- **Delivery reconciliation when a webhook drops**
- **Health verdict per deployment**

*E-mail is the one channel today.*

## Your layer is yours. Our releases arrive as pinned packages

Figure: Four layers, top to bottom. Emphasised, your application: brand, copy, market rules, integrations. The neutral starter, a source application rather than an npm package. The pinned OpenLup packages. Then the named ports, and below them the opt-in adapters on two rails: the managed rail, and the portable rail with sandbox payments only.

- Your application · brand · copy · market rules · integrations
- neutral starter (examples/neutral-store) · a source application, not an npm package
- @openlup/core · @openlup/outbox · @openlup/platform · pinned packages · npm 0.37.0
- ports: payments · fulfilment · invoicing · e-mail · auth · storage
- opt-in adapters
  - managed rail: Supabase PostgreSQL · Supabase Auth · Supabase Storage · S3-compatible object storage · Vercel · Resend email · Klaviyo marketing sync · Stripe payments · Tpay payments · OmniPack warehouse fulfillment · External sales channels
  - portable rail (sandbox payments only): Portable PostgreSQL · Generic OIDC and JWKS identity · S3-compatible object storage · Generic Node host and OCI image · Resend email · Klaviyo marketing sync · External sales channels

| Adapter | On npm | Lane | Proof scope |
| --- | --- | --- | --- |
| Supabase PostgreSQL (managed database) | yes | managed | Data gateway and job ledger for the managed rail. Fresh-install, upgrade and existing-store restore proofs for this rail remain partial. |
| Supabase Auth | yes | managed | Customer and operator sign-in on the managed rail. Operator sign-in smoke checks and a hosted settings readback remain open. |
| Supabase Storage | yes | managed | Blob storage binding. Serving and access proofs remain open. |
| Portable PostgreSQL (self-host and Amazon RDS recipes) | yes | portable | Local self-host recipe receipt, local store transfer and restore witnesses, and one hosted RDS PostgreSQL 16 recipe run on a single synthetic instance. No hosted transfer or recovery has run, and none of this is production qualification. |
| Neon | no | portable | No receipt yet. A Neon recipe receipt and Neon to Amazon RDS transfer and recovery are planned in the hostability round. |
| Generic OIDC and JWKS identity | yes | portable | Opt-in access-token verifier. A real-issuer lifecycle witness passed on one disposable hosted origin; no production qualification. |
| S3-compatible object storage | yes | portable and managed | Local recipe with two gateway accounts and one hosted run on a single synthetic bucket in one region. No production qualification. |
| Vercel (Node functions, scheduled jobs, Blob) | yes | managed | Blob binding and the reference default bundle with the managed database, without the Edge runtime. A Vercel host for the portable neutral store is planned in the hostability round. |
| Generic Node host and OCI image | yes | portable | Local self-host receipt and one hosted run on a single synthetic VM with restart, drain and A to B to A rollback checks. No production qualification. |
| Netlify (independent PaaS candidate) | no | portable | Candidate only, not yet measured. It is evaluated in the hostability round. |
| Resend email | yes | portable and managed | Opt-in adapter on the communications entry with offline conformance. No provider-account evidence. |
| Klaviyo marketing sync | yes | portable and managed | Opt-in adapter with offline conformance only; account verification is pending. |
| Stripe payments | yes | managed | Opt-in example adapter with an injected client. Refund creation and split authorization and capture are not available, some hosted paths are not observed, and real money runs only on the managed rail. |
| Tpay payments | yes | managed | Opt-in example adapter with simulator evidence only. Real settlement and provider readback are not verified. |
| OmniPack warehouse fulfillment | yes | managed | Opt-in, default-off example with offline conformance. Exact absence, label create and void and cancellation are not available, and some dispatch and reconciliation paths have not been exercised. |
| Polish company registries (VAT whitelist, GUS/CEIDG) | no | portable and managed | Not packaged. A packaged example lookup adapter comes in the A5 adapter round. |
| External sales channels (normalized import) | yes | portable and managed | Normalized ingest contracts and bindings. No real connector ships, and conversion to a physical order refuses on the portable rail. |

*Proof scope as the OpenLup roadmap states it; passing conformance does not qualify a provider.*

**Where it runs**

Three runtime bundles: Vercel with Supabase, Node with Supabase, Node with plain PostgreSQL. None is a supported production recipe yet. Real money runs on the managed rail today; a portable real-money rail is on the roadmap. No hosted service exists.

## Built with AI agents, and built to be extended with them

Agents work within explicit human authority. A human is answerable for every line.

- **Dependencies point inward**
- **No I/O, no ambient clock in the kernel**
- **Engine refusals are values, not exceptions**

*The properties that make a codebase legible to an agent.*

> OpenLup is built with Claude Code and Codex: they implement, verify and review, and every change stays within the maintainer’s grant.

## Where the subscription layer lives

| Product | Kind | Subscriptions, dunning, self-service | Published fees | Read |
| --- | --- | --- | --- | --- |
| Saleor | open-source platform, BSD-3-Clause | no subscription engine; recurring payments built on the Transaction API | none; self-hosted | 2026-11-06 |
| WooCommerce Subscriptions | paid extension for WooCommerce, by Woo | subscriptions; retries off by default; self-service in My Account | $279 per year | 2026-11-06 |
| Medusa | open-source platform, MIT | not in core; a documented recipe | none; self-hosted | 2026-11-06 |
| Sylius | open-source platform, MIT; paid Plus module | subscriptions in Sylius Plus (0.x); dunning not documented | Sylius Plus from €800 per year | 2026-11-06 |
| Shopware | MIT Community; commercial plans | Beyond plan subscriptions; pause, cancel; dunning not documented | Beyond, custom price | 2026-11-06 |
| Stripe Billing | billing engine, SaaS; no storefront or fulfilment | subscriptions; Smart Retries; customer portal | 0.7% of billing volume or from $620/month (US) | 2026-11-06 |
| Shopify Subscriptions | Shopify app | subscriptions; configurable retries; customer self-service | free app on a Shopify plan from $39/month (US) | 2026-11-06 |
| Recharge | Shopify app | subscriptions; dunning in every plan; customer portal | from $149/month + 1.50% + 20¢ per order | 2026-11-06 |
| Loop | Shopify app | subscriptions; 15 retries on Starter; customer portal | free plan; from $99/month + 1.0% per order | 2026-11-06 |
| JUO | Shopify app | subscriptions; dunning not documented | from $199/month + 1.25% + 19¢ per order | 2026-11-06 |
| OpenLup | open-source platform, Apache-2.0 | subscriptions, dunning and self-service in the open core | none per order; self-hosted | policy |

*Feature presence and published fees only, read on each product’s own pages on the date shown; card processing is extra everywhere. One deployment serves one store today.*

## Written down before there was money at stake

- **One-way ratchet** — Open never becomes paid (`.github/TIER_POLICY.md`)
- **Paid value, if ever** — In the operational layer, never in withheld code (`.github/TIER_POLICY.md`)
- **DCO, no CLA** — There is no CLA, and there will not be one (`.github/GOVERNANCE.md`)
- **AI contributions welcome** — Disclosure, never a ban (`.github/AI_CONTRIBUTION_POLICY.md`)
- **No SLA, said plainly** — None is offered today (`.github/SUPPORT.md`)
- **Fork is part of the offer** — Apache-2.0, fresh-history root (`.github/PUBLICATION_COMPLETENESS.md`)

> Commerce is strictly additive: paid work is built on top of a stabilized open core, never carved out of it. There are no tiers. Everything the project publishes is open.

**Read the six sentences in full**

Anything published under Apache-2.0 stays open. Permanently. Open never becomes paid. In the operational layer, never in withheld code. There is no CLA, and there will not be one. AI-assisted contributions are welcome. They are not banned, and they will not be banned. Nothing in this project carries a service-level agreement. The first public OpenLup root is a mechanically derived, fresh-history subset of the pre-split development repository, not a hand-assembled copy.

## Built for a real shop first, published second

Not designed in the abstract. Extracted from the author’s own shop, velipet.com, which runs it in production with real customers and real money.

- 113 — platform migrations
- 227 — pgTAP files
- 2 389 — test files
- 23 — server domains
- 18 — core entry points
- 3 — runtime bundles

*Measured on npm 0.37.0. velipet.com is the author’s shop, not an independent adopter; it adopts the published packages, and its changes go through OpenLup’s own review and releases.*

> Subscriptions are the business. They should not be a monthly app fee and a revenue share. I built OpenLup so a brand can own them.
>
> — Bart Roszkowski, author of OpenLup, co-founder of Vue Storefront (now Alokai)

## What has to be true before this is called stable

Figure: The roadmap on one rail: M19 in four releases, a working store from npm, subscription care, payment recovery and account, and the subscription editor; then real payments on the portable rail, hostability, promotion codes and market adapters; and the P1-SF gate as the terminal step. Each step carries its status from the public roadmap.

- M19 — M19a: Working store from npm (In progress)
- M19b1a: Subscription care (Planned)
- M19b1b: Payment recovery and account (Planned)
- M19b2: Subscription editor (Planned)
- After M19 — N6: Real payments on the portable rail (Planned)
- Hostability: Hosting and provider portability (Planned)
- Promotions: Codes and acquisition schedule (Planned)
- A5: Market adapters (Planned)
- Then: the P1-SF gate (Planned)

## Three doors

- [**The engine contract**](https://github.com/openlup/openlup/blob/openlup-core-v0.37.0/packages/core/docs/SUBSCRIPTION_ENGINE.md) (Live) — Cycles, refusals, the ladder
- [**The architecture**](https://github.com/openlup/openlup/blob/openlup-core-v0.37.0/docs/platform/ARCHITECTURE_AND_EXTENSIONS.md) (Live) — Ports, adapters, your layer
- [**The governance**](/governance/) (Live) — Who decides, and how work becomes official

## FAQ

**Can I use it today?**

To evaluate a whole store, yes: the neutral starter installs from npm on PostgreSQL, with sandbox payments. Real money runs on the managed rail, and nothing is stable, supported or certified yet.

**What stays free?**

Everything published under Apache-2.0, permanently. Subscriptions, renewals, dunning, mandates, recovery and messaging will not become paid tiers.

**Will there be a paid version?**

No tiers today. Paid work, if it comes, is strictly additive: built on top of a stabilized open core, never carved out of it.

**What do I build myself?**

Brand, copy, storefront, catalogue, your market's taxes and carriers, your ERP and CRM integrations, your pricing rules, your hosting.

**Which providers work?**

None are certified. Example adapters exist for payments, fulfilment, e-mail, auth and storage, each with its proof scope written down.

**Is it multi-brand?**

One deployment serves one store today.

**Who runs it in production?**

velipet.com, the author’s own shop. It is not an independent adopter.

**Can I contribute with an AI agent?**

Yes. Disclose it and stay accountable for every line. An agent opens a pull request only with a maintainer’s explicit authority.

**Can I fork it?**

Yes. Apache-2.0 and a fresh-history root. The name and logo are not part of the licence, so a fork takes its own name.

## Open source subscription commerce for physical goods

Development preview. Stable begins at the P1-SF gate.

- [View the source](https://github.com/openlup/openlup)

- [Read the governance](/governance/)
