docs: verify pro quota store release notes (#364)

Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
This commit is contained in:
Jasper Van
2026-05-05 17:13:54 -04:00
committed by GitHub
parent 5eac0f4642
commit 05b14a4991
2 changed files with 167 additions and 27 deletions
+80 -19
View File
@@ -1,14 +1,16 @@
# v2.6 — Pro Launch
Introduce ZPan Pro. Ship the cloud-account binding flow, the entitlement verification layer, the feature-gating framework, and a single visible Pro feature — white-label — so there is something concrete to sell on day one.
Introduce ZPan Pro. Ship the cloud-account binding flow, the entitlement verification layer, the feature-gating framework, a self-service quota store, and a visible Pro feature — white-label — so there is something concrete to sell on day one.
**Activation is never local.** Purchase, subscription management, and redemption-code activation all happen on `cloud.zpan.app`. ZPan instances only *bind* to a cloud account; their Pro status mirrors the bound account's entitlement automatically. There is no token to paste and no redemption code field inside ZPan.
**Generic Pro activation remains Cloud-owned.** Subscription purchase, subscription management, generic Pro activation, membership redemption codes, and entitlement source-of-truth all live on `cloud.zpan.space`. ZPan instances only _bind_ to a cloud account for Pro status; their Pro status mirrors the bound account's entitlement automatically. There is no generic Pro token to paste into ZPan.
**Product direction update:** v2.6 also adds a scoped storage-quota store inside ZPan. This does not move generic Pro activation into ZPan. It lets a Pro instance operator define a per-instance storage package catalog, while Cloud remains the merchant of record and Stripe integration owner. Terminal storage buyers do not need ZPan Cloud accounts.
Pro features can be added continuously in later versions. This version's job is to make the **machinery** for paid features exist.
## Depends on
- [cloud v1.0](https://github.com/saltbo/zpan-cloud/blob/main/docs/roadmap/v1.0.md) — cloud accounts, instance pairing, cloud dashboard (purchase + redemption + subscription management), entitlement API must be live
- [cloud v1.0](https://github.com/saltbo/zpan-cloud/blob/main/docs/roadmap/v1.0.md) — cloud accounts, instance pairing, cloud dashboard (subscription purchase + membership redemption + subscription management), entitlement API, anonymous storage checkout, storage-code redemption, and signed quota delivery callbacks must be live
## Major Features
@@ -19,16 +21,17 @@ The only licensing action exposed in ZPan is "Connect to ZPan Cloud". Everything
**Binding flow (device-code style)**:
1. Admin clicks "Connect to Cloud" in `Settings → Billing`
2. ZPan calls `POST cloud.zpan.app/api/pairings` — receives a short human-readable code (e.g. `ABC-123`) and a pairing URL
3. ZPan displays: "Visit **cloud.zpan.app/pair** and enter code `ABC-123`"
4. User signs in (or signs up) on cloud.zpan.app, enters the code, confirms the pairing (instance host + instance name shown for verification)
5. ZPan polls `GET cloud.zpan.app/api/pairings/{code}` every 5 seconds
2. ZPan calls `POST cloud.zpan.space/api/pairings` — receives a short human-readable code (e.g. `ABC-123`) and a pairing URL
3. ZPan displays: "Visit **cloud.zpan.space/pair** and enter code `ABC-123`"
4. User signs in (or signs up) on cloud.zpan.space, enters the code, confirms the pairing (instance host + instance name shown for verification)
5. ZPan polls `GET cloud.zpan.space/api/pairings/{code}` every 5 seconds
6. Once the user approves, cloud returns a long-lived refresh token + the first entitlement certificate
7. ZPan stores the refresh token in its database (admin-only read)
Device code is chosen over OAuth redirect because ZPan instances often run on internal networks, homelab IPs, or behind NAT — requiring a public return URL would break those deployments.
**Disconnect** (either side can initiate):
- In ZPan: "Disconnect from Cloud" — deletes local credentials, entitlement certificate is discarded, feature gates lock immediately
- In cloud dashboard: "Unbind instance" — ZPan's next refresh returns `401 Unbound`, feature gates lock on that refresh
@@ -58,21 +61,63 @@ Frontend components:
### Billing Dashboard
`Settings → Billing` page. Two states only — bound or unbound. Zero purchase UI, zero redemption UI.
`Settings → Billing` page. Two states only — bound or unbound. Zero generic Pro purchase UI, zero membership redemption UI.
**If unbound (Free)**:
- Comparison table: Free vs Pro feature list
- Primary CTA: "Connect to ZPan Cloud" → launches the device-code binding flow
- Short explainer: "Create or sign in to your ZPan Cloud account, then activate Pro there. This instance will mirror your account's Pro status automatically."
**If bound**:
- Cloud account identity (email or username, fetched from cloud)
- Plan name + current entitlement expiry (next refresh time shown for transparency)
- Feature list (every feature unlocked under the current plan)
- Primary CTA: "Manage on Cloud" → opens `cloud.zpan.app/dashboard` in a new tab (this is where the user does purchase, renewal, payment method changes, invoices, redemption-code entry)
- Primary CTA: "Manage on Cloud" → opens `cloud.zpan.space/dashboard` in a new tab (this is where the user does Pro purchase, renewal, payment method changes, invoices, and membership redemption-code entry)
- Secondary CTA: "Disconnect from Cloud"
- Last-refresh timestamp + manual "Refresh now" button (for support scenarios where a just-completed purchase on cloud should reflect immediately)
### Pro Quota Store
The quota store is a scoped addition to the original v2.6 Pro plan. It gives Pro operators a storage upsell path for their own instance users without making those users create Cloud accounts.
Ownership:
- **Cloud is merchant of record in v2.6.** Cloud owns Stripe Checkout, Stripe paid webhooks, storage-code inventory, and delivery retries.
- **The ZPan operator defines the package catalog per instance.** Package name, description, bytes, amount, currency, active state, and sort order are configured in ZPan admin.
- **Terminal buyers do not need Cloud accounts.** ZPan creates signed purchase or redemption sessions and sends the user to Cloud only for checkout/redeem handling.
- **Quota grants are permanent additive credits in v2.6.** For finite base quotas, effective quota is `base org quota + active quota grants`; admin base quota edits do not erase paid grants. The existing `0` base quota sentinel still means unlimited.
- **Store requires a public HTTPS Cloud callback endpoint while enabled.** Admin configures the public ZPan URL and the shared signing secret so Cloud can call `POST /api/quota-store/webhooks/cloud`.
Admin flow:
1. Pro admin opens `Settings -> Quota Store`
2. Admin enables the store, sets Cloud base URL, public instance URL, and webhook signing secret
3. ZPan displays the callback URL Cloud must reach over public HTTPS
4. Admin creates active storage packages
5. ZPan syncs the catalog to Cloud through `POST /api/store/packages/sync` using `x-zpan-store-*` signatures
6. Admin confirms each package has synced Cloud status before it becomes purchasable
Terminal-user flow:
1. User opens the ZPan Store from the quota meter without signing in to Cloud
2. User selects a personal or team target org they belong to
3. Checkout calls `POST /api/quota-store/checkout`, which validates target access and creates a signed Cloud storage session
4. Cloud creates Stripe Checkout from the package snapshot
5. Stripe paid event marks the Cloud order paid
6. Cloud sends a signed `x-zpan-cloud-*` delivery callback to ZPan
7. ZPan verifies the signature, records the delivery event idempotently, appends one quota grant, and increases the target org's effective quota
8. The quota meter and grant history refresh
9. If the user had a draft upload blocked by quota, confirm/upload can be retried while the draft still exists
Storage-code redemption:
- ZPan Store accepts storage codes separately from membership redemption.
- The user selects the target org, submits the code, and ZPan sends a signed redemption session to Cloud.
- Cloud validates storage-code inventory and delivers quota back through the same signed callback path.
- Membership redemption remains Cloud dashboard behavior and does not share this ZPan Store flow.
### First Pro Feature: White-Label
Pure client-side. No cloud dependency beyond the entitlement gate. Demonstrates upgrade value visually.
@@ -85,6 +130,7 @@ Pure client-side. No cloud dependency beyond the entitlement gate. Demonstrates
- **Admin page** — `Settings → Branding`: upload logo, favicon; edit wordmark text; preview live
Deliberately excluded from v2.6 (deferred to later Pro versions):
- Custom brand colors and fonts
- Per-tenant branding (when multi-tenant ships)
- Custom email templates
@@ -96,22 +142,26 @@ Features shipped in earlier versions that are **operator-grade** (help you run Z
Three gates in v2.6:
**1. Open registration mode** (from v2.1)
- Registration modes `closed` and `invite-only` remain **Free**.
- Registration mode `open` (anyone can sign up via the public form) becomes **Pro**.
- Without a Pro license, `open` is rejected; registration mode falls back to `invite-only`.
**2. Team count ≤ 3** (from v2.2)
**2. Team count > 1 extra team** (from v2.2)
- Free instances can create **1 personal workspace plus up to 1 additional team**.
- 3rd org creation is blocked in UI; API returns 402.
- Rationale: a single person or family team doesn't need more than 3 team workspaces; anyone building past that is running ZPan as a platform.
- Rationale: a single person or family team doesn't need more than one shared team workspace; anyone building past that is running ZPan as a platform.
**3. Storage backend count**
**3. Storage backend count**
- Free instances can add up to **3 storages**.
- Adding a 4th storage requires **Pro**.
Everything else from v2.0v2.5 stays Free:
- Social login, OIDC (single IdP), invite codes (single and bulk generation)
- Team workspaces (up to 3), shared folders, member roles, activity feed, public user profile
- Team workspaces within the Free limit, shared folders, member roles, activity feed, public user profile
- All sharing features (password, expiration, download limits, direct links)
- All image hosting (upload API, PicGo / ShareX integration, per-org custom domain, hotlink protection)
- All 7 deployment targets
@@ -119,9 +169,9 @@ Everything else from v2.0v2.5 stays Free:
## Non-goals for v2.6
- **Token paste activation** — the old model of "purchase → receive token email → paste into ZPan" is deliberately replaced by account binding
- **In-ZPan purchase UI** — all purchase flows live on `cloud.zpan.app`
- **In-ZPan redemption code input** — redemption also lives on cloud dashboard
- **Metered / usage-based billing** — no cloud-dependent features yet, not needed
- **In-ZPan generic Pro purchase UI** — Pro subscription purchase flows live on `cloud.zpan.space`
- **In-ZPan generic membership redemption input** — membership redemption lives on the cloud dashboard; only storage-code redemption is accepted in the ZPan quota store
- **Metered / usage-based billing** — quota packages are fixed additive credits in v2.6, not usage-metered subscriptions
- **Team / Business tiers** — single Pro SKU only
- **Multi-seat** — one Pro subscription binds one ZPan instance
- **Multi-bind per account** — a single Pro subscription can only bind one active instance. Binding a second instance on the same Pro account requires unbinding the first, or a future Team plan
@@ -131,15 +181,26 @@ Everything else from v2.0v2.5 stays Free:
## User Scenarios
**Indie dev launching a paid service:**
> I run ZPan on my VPS. I click "Connect to Cloud", see a pairing code, go to cloud.zpan.app/pair on my laptop, sign in with GitHub, confirm "Bind instance at files.mycompany.com". Back in ZPan the UI shows "Bound to alice@example.com · Plan: Free". I click "Manage on Cloud", subscribe to Pro, and within 30 seconds my ZPan lights up as Pro — I go to Settings → Branding, upload my logo, and my instance looks like my product.
> I run ZPan on my VPS. I click "Connect to Cloud", see a pairing code, go to cloud.zpan.space/pair on my laptop, sign in with GitHub, confirm "Bind instance at files.mycompany.com". Back in ZPan the UI shows "Bound to alice@example.com · Plan: Free". I click "Manage on Cloud", subscribe to Pro, and within 30 seconds my ZPan lights up as Pro — I go to Settings → Branding, upload my logo, and my instance looks like my product.
**Contributor who got a code:**
> I submitted a solid PR. The maintainer sends me a ZPan Pro code good for 1 year. I already have a cloud.zpan.app account, so I sign in there, paste the code on the "Redeem" page. My cloud account is now Pro. I go back to my self-hosted ZPan, click "Connect to Cloud", pair it to my account, Pro unlocks. No code ever touched my ZPan instance.
> I submitted a solid PR. The maintainer sends me a ZPan Pro code good for 1 year. I already have a cloud.zpan.space account, so I sign in there, paste the code on the "Redeem" page. My cloud account is now Pro. I go back to my self-hosted ZPan, click "Connect to Cloud", pair it to my account, Pro unlocks. No code ever touched my ZPan instance.
**Terminal user buying more storage:**
> I am a regular user on someone else's ZPan instance and my upload hits quota. I click Add storage from the quota meter, choose my personal workspace or team, and pick a storage package. Cloud opens Stripe Checkout without asking me to create a Cloud account. After payment, Cloud sends the signed delivery callback and my ZPan quota meter shows the purchased storage. I retry the blocked upload while the draft is still present.
**Terminal user redeeming a storage code:**
> I receive a storage code from the instance operator. In ZPan Store, I select the target team and redeem the code. The storage grant appears in my grant history and the team's effective quota increases. This does not activate Pro membership and does not use the Cloud membership redemption page.
**Free user hits a gate:**
> I try to create a 2nd team workspace. The "New Team" button shows a Pro badge; clicking it opens a dialog: "Free includes one personal workspace plus up to one team. Pro removes the limit. **Connect to Cloud** to upgrade." I click through, sign up for a cloud account, and from the cloud dashboard I either start a trial or redeem a code. The moment my account has Pro, my ZPan instance reflects it.
## v1 Issues Resolved
- **#18** Purchasable storage — reframed as ZPan Pro via bound cloud account (storage is still BYO S3)
- **#18** Purchasable storage — shipped as Pro quota store: Cloud remains merchant of record, ZPan operator defines packages, and storage remains BYO S3
- **#161** Custom logo — shipped as the first Pro feature under white-label
+87 -8
View File
@@ -2,7 +2,9 @@
## What's New
ZPan v2.6 introduces **ZPan Pro** — a paid tier that unlocks operator-grade features for people running ZPan as a platform for others. Free (self-hosted personal use) remains free and unlimited for everything it has always supported.
ZPan v2.6 introduces **ZPan Pro** — a paid tier that unlocks operator-grade features for people running ZPan as a platform for others. Free self-hosted personal use remains supported, with the operator-grade gates noted below.
v2.6 also adds the **Pro quota store**. This is a scoped storage purchase and redemption flow for Pro operators; it does not move generic Pro activation into ZPan. Cloud remains the merchant of record and owns Stripe, subscriptions, membership redemption, and payment operations.
---
@@ -32,6 +34,77 @@ Manage in **Settings → Branding**.
---
## Pro Quota Store
The quota store lets a Pro instance operator sell or grant extra storage to terminal users on that ZPan instance.
### Direction and Ownership
- **Cloud is merchant of record in v2.6.** Stripe Checkout, Stripe webhooks, Cloud order state, and storage-code inventory run through ZPan Cloud.
- **The ZPan operator defines the package catalog per instance.** Admins create packages in ZPan with name, description, bytes, price, currency, active state, and sort order.
- **Terminal buyers do not need Cloud accounts.** ZPan signs purchase and redemption sessions; Cloud uses those sessions for checkout or storage-code redemption.
- **Quota grants are permanent additive credits in v2.6.** For finite base quotas, effective quota is base org quota plus active grants. Editing the admin base quota does not erase paid or redeemed grants. The existing `0` base quota sentinel still means unlimited.
- **Generic Pro activation stays Cloud-owned.** Subscription purchase, membership redemption, and Pro entitlement activation remain in the Cloud dashboard.
### Admin Setup
1. Bind the ZPan instance to a Pro Cloud entitlement.
2. Open the admin quota store settings.
3. Enable the store.
4. Set the Cloud base URL, the public ZPan instance URL, and the webhook signing secret.
5. Confirm the generated callback URL is publicly reachable over HTTPS: `/api/quota-store/webhooks/cloud`.
6. Create active packages and confirm their Cloud sync status is `synced`.
The user-facing store remains unavailable while disabled or while the webhook signing secret is missing.
### Buyer Flow
1. A terminal user opens **Store** from the quota meter.
2. The user chooses a personal or team target org.
3. Checkout calls ZPan, then Cloud creates Stripe Checkout from the signed package snapshot.
4. After Stripe marks the Cloud order paid, Cloud delivers the quota grant to ZPan through a signed callback.
5. ZPan records the delivery idempotently, adds one immutable quota grant, and refreshes effective quota, quota meter, and grant history.
Users who previously hit quota can retry confirm/upload while the draft still exists after the grant arrives.
### Storage-Code Redemption
Storage-code redemption is separate from Cloud membership redemption:
- Users redeem storage codes in ZPan Store against a selected personal or team org.
- Cloud validates the storage code and sends quota delivery through the same signed callback path.
- Membership redemption remains on Cloud and does not share this flow.
### Verification Notes
The merged implementation covers:
- Pro gating for quota store admin and self-service APIs
- Package validation, Cloud catalog sync, and Cloud sync error surfacing
- Target org access checks for checkout and redemption
- Anonymous Cloud checkout handoff from signed ZPan storage sessions
- Stripe-paid Cloud order delivery through signed callback
- Duplicate Cloud delivery returning success without a second grant
- Bad Cloud webhook signatures returning `401`
- Admin base quota edits preserving paid grants in effective quota
- Upload confirm quota checks using base quota plus active grants
- Storage-code redemption using storage-code endpoints, not membership redemption
Manual preview evidence was captured in the quota-store UI work:
1. Open admin quota-store settings, enable the store, configure Cloud base URL, public instance URL, and webhook signing secret, then create an active package and confirm Cloud sync status.
- Screenshot: `/tmp/zpan-quota-store-preview/admin-package-store.png`
2. Open the terminal-user Store from the quota meter without a Cloud account, select a personal or team target org, and confirm active packages render.
- Screenshot: `/tmp/zpan-quota-store-preview/user-store-packages.png`
3. Start checkout and confirm ZPan opens the Cloud/Stripe checkout URL from the signed storage session.
- Screenshot: `/tmp/zpan-quota-store-preview/checkout-launch.png`
4. Redeem a storage code for the selected org and confirm grant history plus quota meter refresh.
- Screenshot: `/tmp/zpan-quota-store-preview/redemption-quota-update.png`
Backend verification covered duplicate Cloud delivery not double-granting quota, bad webhook signatures returning `401`, and admin base quota edits preserving paid grants.
---
## Retroactive Pro Gates
Three features from earlier versions have moved to **Pro** in v2.6. These affect only operators running ZPan as a platform for unknown third parties.
@@ -54,13 +127,14 @@ Free instances may configure up to **3 storage backends**. Adding a 4th storage
## Feature Gate Overview
| Feature | Free | Pro |
|---------|-----------|-----|
| All v2.0v2.5 features | ✅ | ✅ |
| Open registration mode | — | ✅ |
| Teams beyond 3 | — | ✅ |
| Unlimited storages | Up to 3 | ✅ |
| White-label branding | — | ✅ |
| Feature | Free | Pro |
| ------------------------- | ------- | --- |
| All v2.0v2.5 features | ✅ | ✅ |
| Open registration mode | — | ✅ |
| Teams beyond 1 extra team | — | ✅ |
| Unlimited storages | Up to 3 | ✅ |
| White-label branding | — | ✅ |
| Quota store | — | ✅ |
---
@@ -70,6 +144,7 @@ Free instances may configure up to **3 storage backends**. Adding a 4th storage
2. Restart your container — migrations run automatically.
3. If you use **open registration mode**, bind a Pro license before restarting (see above).
4. Optional: go to **Settings → Billing** to connect your cloud account and activate Pro.
5. Optional for Pro operators: enable **Quota Store** only after configuring a public HTTPS callback URL and webhook signing secret.
For Cloudflare Workers deployments, trigger the `Deploy to Cloudflare Workers` workflow from your fork's Actions tab.
@@ -89,6 +164,10 @@ None at release time.
- `feat(ui)`: Settings → Billing page (bound / unbound states)
- `feat(ui)`: `<ProBadge />` and `<UpgradeHint />` frontend primitives
- `feat(branding)`: White-label settings page (logo, favicon, wordmark, footer toggle)
- `feat(quota-store)`: Pro-gated admin package catalog and store settings with Cloud sync status
- `feat(quota-store)`: Terminal-user storage checkout, target org selection, storage-code redemption, and grant history
- `feat(quota-store)`: Signed Cloud delivery callback with idempotent quota grants
- `feat(quota-store)`: Effective quota now includes permanent additive storage grants
- `feat(gates)`: Open registration gated behind `open_registration` Pro feature
- `feat(gates)`: Team creation blocked after 1 extra team for Free (`teams_unlimited` required for the 3rd org)
- `feat(gates)`: Storage creation blocked at 3 for Free (`storages_unlimited` required for the 4th storage)