* fix(analytics): enforce trustworthy offline statistics Separate immutable counters from point-in-time snapshots, expose incomplete coverage instead of synthetic zeroes, and keep browser analytics result-only. Restore finite Free quota baselines, fail closed for invalid storage quota, reconcile traffic reports fairly, and add production-safe backfill and data-quality diagnostics. * fix(analytics): preserve global backfill totals Group generated hourly backfill rows by their projected values so SQLite cannot resolve output aliases to source organization columns and overwrite cross-organization totals.
19 KiB
v2.6 — Pro / Business Launch
Introduce ZPan paid tiers. Ship the cloud-account binding flow, the entitlement verification layer, the feature-gating framework, a self-service quota store, and visible paid features so there is something concrete to sell on day one.
Generic paid-tier activation remains Cloud-owned. Subscription purchase, subscription management, generic Pro / Business activation, membership redemption codes, and entitlement source-of-truth all live on cloud.zpan.space. ZPan instances only bind to a cloud account for paid-tier status; their status mirrors the bound account's entitlement automatically. There is no generic token to paste into ZPan.
Product direction update: v2.6 also adds a scoped storage-quota store inside ZPan. This does not move generic paid-tier activation into ZPan. It lets a Business 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.
Scope update: audit log and site announcements have moved forward from the original v2.7 operator plan into v2.6. v2.7 is now reserved for file-processing workflows: small-file archive handling in Community and remote-download orchestration through user-owned download engines such as Aria2 and qBittorrent.
Paid-tier 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 — cloud accounts, instance pairing, cloud dashboard (subscription purchase + membership redemption + subscription management), entitlement API, anonymous storage checkout, gift-card credit redemption, and binding-authenticated quota-change webhooks must be live
Major Features
Cloud Account Binding
The only licensing action exposed in ZPan is "Connect to ZPan Cloud". Everything else lives on the cloud side.
Binding flow (device-code style):
- Admin clicks "Connect to Cloud" in
Settings → Billing - ZPan calls
POST cloud.zpan.space/api/pairings— receives a short human-readable code (e.g.ABC-123) and a pairing URL - ZPan displays: "Visit cloud.zpan.space/pair and enter code
ABC-123" - User signs in (or signs up) on cloud.zpan.space, enters the code, confirms the pairing (instance host + instance name shown for verification)
- ZPan polls
GET cloud.zpan.space/api/pairings/{code}every 5 seconds - Once the user approves, cloud returns a long-lived refresh token + the first entitlement certificate
- 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
Entitlement System
Cloud is the source of truth. ZPan fetches, caches locally, and verifies offline.
- Entitlement certificate — Ed25519-signed document from cloud containing
account_id,plan,features[],instance_id,issued_at,expires_at(24-hour TTL) - Ed25519 public key embedded in every ZPan build at compile time — verification is fully local, zero network calls per feature check
- Refresh cadence — a background task refreshes the certificate every 6 hours using the stored refresh token
- Offline tolerance — because the certificate is valid for 24 hours, a ZPan instance can stay offline up to 24 hours and still enforce Pro correctly
- Hard fail after expiry — once the cached certificate's
expires_atpasses and cloud is unreachable, feature gates fall back to Free until the next successful refresh - Revocation is implicit — there is no separate revocation list. Because certificates are short-lived, unbinding on cloud takes effect within 24 hours at worst (or instantly if the ZPan instance is online at the next scheduled refresh)
Feature Gate Framework
New module server/licensing/ with three primitives:
verify(cert)— Ed25519 signature check + expiry check against the cached certificaterequireFeature(name)— middleware for API routes; returns402 Payment Requiredwhen the feature is not in the current entitlementhasFeature(name)— synchronous boolean for UI conditional rendering
Frontend components:
<ProBadge />— small "Pro" tag rendered next to gated features in menus / settings<UpgradeHint feature="..." />— block-level upsell shown in place of a gated UI region; CTA goes to "Connect to Cloud" (if unbound) or "Manage on Cloud" (if bound but feature not in plan)
Billing Dashboard
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 vs Business 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.space/dashboardin 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)
Business Quota Store
The quota store is a scoped addition to the v2.6 paid-tier plan. It gives Business 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 subscriptions, Stripe paid webhooks, credit grants, gift-card redemption, usage debit, order state, and webhook 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 checkout requests through the existing paid-tier Cloud binding and sends the user to Cloud only for checkout handling.
- Purchased storage and traffic are delivered as Cloud entitlements in v2.6. Every organization keeps a Free baseline plan. An active paid plan overrides that baseline while grants add to it; expiry or revocation therefore falls back to Free automatically. Missing or zero effective storage entitlement fails closed and never means unlimited storage.
- A scheduled, idempotent reconciler restores missing or accidentally revoked Free baselines before quota snapshots while preserving existing baseline sizes.
- Monthly storage package subscriptions stay Cloud-owned. Cloud owns Stripe subscriptions and sends authenticated
order.quota_changeddeliveries only for entitlement-changing subscription states: initial entitlement, positive create/update deltas, renewals that replace the current entitlement period, and terminal reversals. ZPan records deliveries idempotently under a stable subscription entitlement id such asstripe_subscription:<subscriptionId>:<orgId>and recomputes effective quota from active entitlements. - Metered traffic billing is credit-backed and usage-event based. Credits are unitless internal ZPan Cloud units, not USD and not Stripe Customer Balance. ZPan increments local monthly traffic usage before issuing a presigned download URL, reports
traffic_egressusage to Cloud with an idempotency key, and denies the request if Cloud returns an insufficient-credit decision. ZPan does not price or debit credits. - Subscriptions and credits are separate. Stripe subscriptions remain normal USD subscriptions and may include credits per billing period. Free or fully couponed subscriptions still grant included credits when the Cloud subscription or invoice state qualifies. Credits can also come from top-up purchases, gift-card redemption, and admin grants, but credits only pay usage-based charges. They do not pay subscriptions or fixed package purchases.
- Store Cloud communication is automatic through the paid-tier binding. ZPan resolves Cloud from
ZPAN_CLOUD_URLor the built-in default, authenticates catalog sync, checkout with the bound instance refresh token, and sends normal JSON request bodies that includeboundLicenseId. - Webhook auth uses Cloud-signed event tokens. Cloud order quota-change webhook attempts include
x-zpan-cloud-event-token, a PASETO token signed by Cloud. ZPan verifies it with the built-in Cloud public keys and requires the current issuer, an audience equal to the target instance/license id, the bound license id claim, order quota-change event id, raw-body payload hash, and short expiry.
Admin flow:
- Business admin opens
Settings -> Quota Store - Admin enables the store from the operator settings panel
- Admin creates active storage packages
- ZPan proxies catalog writes to Cloud through the admin package endpoints using the active Pro binding credentials
- Admin confirms each package has synced Cloud status before it becomes purchasable
Terminal-user flow:
- User opens the ZPan Store from the quota meter without signing in to Cloud
- User selects a personal or team target org they belong to
- Checkout calls
POST /api/store/checkouts, which validates target access and creates a Cloud checkout using the Pro binding refresh token - Cloud creates Stripe Checkout from the package snapshot
- Stripe paid event marks the initial subscription checkout paid, or Cloud accepts a positive subscription update delta
- Cloud sends one authenticated quota-change webhook to ZPan
- ZPan verifies the Cloud event token and raw payload hash, records the order quota-change event idempotently under the stable subscription entitlement id, and stores storage and traffic as active Cloud entitlements for the target org
- Normal subscription renewals are billed in Cloud only; when they change the entitlement period, Cloud sends a replacement delivery that cannot stack quota
- The quota meter and order history refresh
- If the user had a draft upload blocked by quota, confirm/upload can be retried while the draft still exists
Metered traffic caveats:
- ZPan meters download traffic when it issues a presigned URL. S3 serves the object directly afterward, so ZPan cannot observe partial downloads, canceled transfers, range requests, cache hits, or repeated use of an already-issued URL.
- Cloud credit denial happens before ZPan returns the presigned URL. If Cloud rejects usage with
insufficient_credits, ZPan refunds the local traffic counter for that attempt and returns402. - Local monthly traffic quota is still enforced before Cloud reporting. Cloud metering is for credit-backed usage billing, not a replacement for local quota checks.
Gift card checkout:
- ZPan Store accepts gift cards separately from membership redemption.
- The user selects the target org and redeems the gift card through Cloud credit endpoints proxied by ZPan.
- Cloud validates the gift card and grants credits. Cloud sends quota-change webhooks only when a separate package purchase or subscription fulfillment changes ZPan storage or traffic entitlements.
- 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.
- Custom site logo — replaces ZPan wordmark in nav and auth pages
- Custom favicon
- Custom wordmark text — replace "ZPan" in page titles and copy
- Remove "Powered by ZPan" footer — Free deployments keep it, Pro can hide it
- Asset storage — uploaded to
_system/branding/in the instance's own S3 bucket - 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
Site Announcements
Business operators can publish instance-wide announcements without relying on external channels.
- Admin page — create, edit, publish, archive, and delete announcements
- User surface — authenticated users can view active announcements and announcement history
- Content — Markdown body with title, priority, status, and published timestamp
- Gate —
site_announcements
This is local to ZPan and has no Cloud runtime dependency beyond entitlement verification.
Audit Log
Audit log moved forward into v2.6 as an operator-grade Pro feature.
- Tracked actions — uploads, deletes, quota order changes, admin-visible operational events, and other events emitted through the unified activity layer
- Admin page — filterable audit list with pagination and common filters
- Storage — local database tables, indexed for admin review
- Gate —
audit_log
The v2.6 audit log is the local review surface. External event delivery remains a later webhook feature.
Retroactive Paid-Tier Gates
Features shipped in earlier versions that are operator-grade (help you run ZPan as a service for others) become paid-tier gated starting in v2.6. The framework is the same: if you are the administrator of your own instance using ZPan yourself, nothing changes. If you are running ZPan as a platform for unknown third parties, you'll hit these gates.
Three gates in v2.6:
1. Open registration mode (from v2.1)
- Registration modes
closedandinvite-onlyremain Free. - Registration mode
open(anyone can sign up via the public form) becomes Pro. - Without a Pro license,
openis rejected; registration mode falls back toinvite-only.
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 one shared team workspace; anyone building past that is running ZPan as a platform.
3. Storage backend count
- Free instances can add up to 3 storages.
- Adding a 4th storage requires Pro.
Everything else from v2.0–v2.5 stays Free:
- Social login, OIDC (single IdP), invite codes (single and bulk generation)
- 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
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 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 gift-card credit redemption is accepted in the ZPan quota store
- In-ZPan subscription billing engine — storage package subscriptions, metered usage pricing, credit debit, invoices, and payment operations stay on Cloud; ZPan only stores entitlement deliveries, proxies credit reads/redemptions, and reports traffic usage
- Webhook notifications — external event delivery is deferred past v2.6
- Remote download and archive processing — moved to v2.7; remote download is Community orchestration through user-owned engines, not a Pro Cloud downloader
- Complex tier packaging — v2.6 introduces Pro / Business entitlement mechanics, but detailed enterprise packaging continues in later versions
- Multi-seat — one paid subscription binds one ZPan instance
- Multi-bind per account — a single paid subscription can only bind one active instance. Binding a second instance on the same paid account requires unbinding the first, or a future Team plan
- Refund automation — manual via Stripe dashboard on cloud side
- Chinese local payment (Alipay / WeChat Pay) — Stripe-only at launch, evaluated post-launch
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.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.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 authenticated quota-change webhook and my ZPan quota meter shows the purchased storage. I retry the blocked upload while the draft still exists.
Terminal user redeeming a gift card for credits:
I receive a gift card from the instance operator. In ZPan Store, I select the target team and redeem the gift card into Cloud credits. The credit balance and credit activity update, but no storage quota entitlement is delivered by the gift-card redemption itself. Those credits can pay later usage-based traffic charges; they do not activate Pro membership, pay subscriptions, pay fixed package purchases, or 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 — 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