QR Coupon Journeys
Status: proposed design, 18 September 2026. Plan label: design:qr-coupon-journeys. Every implementation issue also carries astra. This document specifies future implementation; examples introduce proposed APIs and must not be copied as working code today.
Start here
This page is the entry point for the complete design package. The requested design, mockups and implementation issues are complete and saved in draft PR #2067. The runtime feature and campaign are not implemented or live. There is no production redemption URL yet.
These source links work on GitHub while the proposal awaits merge; the mesh links elsewhere in the documents describe their eventual node locations.
| Read or inspect | Entry |
|---|---|
| Overall implementation and fluent actions | Continue below on this page |
| Phone card, offer and installation | Phone design |
| Coupon builder, configurable features and COURSES3M | Coupon design |
| Apple Pay, Google Pay, card setup and renewal | Payment design |
| LinkedIn artwork, post copy and prompts | LinkedIn design · PNG artwork |
| Interactive previews | Phone HTML · LinkedIn HTML; GitHub displays source, so download/open locally or use the previews in the design conversation |
| Delivery sequence, model/effort recommendation and all tasks | Implementation backlog · Tracking epic #2066 |
The idea
An activity can prepare a reusable action, publish it at an opaque GUID URL on a chosen MeshWeaver site, and return an Open QR card action. The sender adds that card to their iPhone or Android home screen. Someone else scans it, signs up or logs in on the destination site, and resumes a prefilled page. Visiting the URL prepares the action; the recipient confirms any mutation.
The first application is COURSES3M: a bounded Personal-plan offer with payment-method setup upfront, no activation charge, and an optional, separately approved renewal feature. There is plenty of free content; non-free items require payment and each separate purchase requires confirmation. The renewal checkbox authorizes the disclosed recurring subscription only. See the coupon specification, payment methods, phone design, and issue backlog.
Product contract
- The publisher selects a registered destination site, allowed action, presentation, and permitted prefilled values. A fluent API declares intent; it does not serialize executable C# supplied by a URL visitor.
- Publication runs as an Activity, validates publisher rights, and writes a durable action definition. The result contains a stable URL, card URL, and durable activity action descriptor.
- The card has a large QR code, verified site hostname, offer title, expiry information, Copy link, Share, and Install on iPhone / Android. Installing creates a home-screen web-app icon. It does not install courses or create a native store app.
- Both the card page and the recipient offer are Public but not Anonymous: every signed-in user may open the sanitized presentation without purchasing or enrolling; anonymous visitors see only the sign-in gateway. Management, coupon internals, personal prefills, and claim records remain private.
- Scanning opens the destination site's sign-in gateway. Login, registration, verification, and onboarding retain a server-side continuation. The recipient returns to the offer, sees permitted prefilled fields, and explicitly continues.
- Payment-method setup saves a method for future customer-confirmed purchases. Without the optional renewal choice, setup must not create a PaymentIntent, recurring subscription, invoice, or charge. When the recipient separately opts into renewal, the provider may create a zero-due trial subscription with the exact disclosed renewal schedule, backed by an explicit recurring mandate. Wallet availability is detected; card entry remains the fallback. No branch may charge for activation. Apple Pay saved-token restrictions require a compatible card fallback for default reusable on-session storage; do not silently select renewal to enable a wallet.
- After verified setup and fresh coupon validation, the server activates the bounded plan exactly once, installs the selected eligible course if one was specified, and opens its cover or installed entry point. A plan-only offer may open the course catalog. The destination is checked against current permission and install state.
No outbound email or message is sent merely by creating a link. Copy/Share hands the link to the sender; later messaging integrations require their normal explicit send authorization.
Existing building blocks and gaps
Evidence was read from this repository and the mesh on 18 September 2026. Re-check implementation details when an issue begins.
| Existing building block | Reuse | Missing capability |
|---|---|---|
hub.RunActivity, ActivityContext, owning partition, cancellation and logs |
Publication, provider setup, fulfillment and retries get ordinary Activities | A durable typed navigation action that survives the execution hub unloading |
src/MeshWeaver.Graph.Views/ActivityViews.cs |
Render a current activity's returned controls | Current result controls are recovered from a live kernel stream; persisted return-value JSON is not a durable UI contract |
Essentials/OperationRequest/Source/OperationDsl.cs |
Fluent style and reviewable declarations as inspiration | OperationRequest runs privileged administrative scripts; it is not the execution engine for shared recipient links |
Existing Login, Welcome, Onboarding and Navigation/LocalUrl.cs |
Safe local return destinations and existing account creation | Durable continuation across all auth/onboarding steps and standalone app launches |
| Framework controls and navigation | Bind forms to typed streams; use existing button/navigation primitives | QR, PWA install guidance, and reusable Stripe setup controls |
Store/Coupon, Licensing, billing, payments and installers |
Domain validation and fulfillment primitives | Payment setup contract, exact-once claim orchestration, and the coupon fluent adapter |
The activity skill's general persistence rule still applies: save results on a domain node. The activity action descriptor links to that node; it must never make historical activity rendering depend on a still-live execution hub.
Ownership and dependency direction
Essentials already requires Store. Do not add Store -> Essentials: that would introduce a package cycle.
- Core
MeshWeaver.Mesh.Contract(proposed minimal seam): immutable serializable action descriptor, invocation, handler capability contract, and action-reference result shared by activity views and plugins. No coupon, Stripe, PWA, or Essentials-specific behavior belongs in core. Review this small public API before implementation. - Essentials: publication, typed action-link records, registered-site lookup, recipient routing and continuation, presentation, QR card, and composition of domain actions. Put node implementations under proposed
Essentials/ActionLinkand relatedSource/andTest/folders. - Store/Coupon: coupon builder, coupon action payload/handler, immutable claim terms and claim orchestration. It emits a neutral invocation and never imports Essentials runtime types.
- Portal/Blazor and Graph.Views in this repo: minimal route, durable activity rendering, manifest/install and payment-control integration. Reuse actual framework controls and host extension points.
- Core payments contract and Stripe provider: extend the existing provider boundary with setup semantics as described in the payment document, then consume it in Store.
Composition is in Essentials, which already has Store available. Handler registration is explicit by key and schema version. Publication fails with a useful capability error if the destination lacks the handler or required setup capability. An advertised action cannot rely on a module being accidentally loaded.
Proposed fluent API
All names in this block are proposed, not current MeshWeaver APIs. site and coursePath are resolved registry objects/paths supplied by the publisher; no sample silently chooses a course or site for production.
// Proposed API. Neither constructing the descriptor nor rendering the card executes it.
var couponAction = Coupons.Journey("COURSES3M")
.OpenCourse(coursePath)
.Benefit(b => b.UseCouponTierAndExactExpiry())
.RequirePaymentSetup(s => s
.ForIndividuallyApprovedPurchases()
.OfferEligibleWalletsAndCard()
.ConsentText("Save a payment method for purchases I choose. "
+ "Nothing is charged today."))
.WithFeatures(f => f
.OptionalSubscriptionRenewal("continue-personal", r => r
.ForCouponTier("personal")
.DefaultSelected(false)
.StartAtAcceptedBenefitEnd()
.QuoteCurrentPlan("personal", cadence: "monthly")
.RequireSeparateRecurringConsent()
.ShowStartTotalCadenceAndCancellation()))
.AfterClaim(a => a.OpenSelectedCourse())
.BuildAction();
// Proposed Essentials composer, outside Store's dependency graph.
return Actions.Link("courses3m")
.ForSite(site)
.RequireSignIn()
.Do(couponAction)
.Prefill(p => p
.Language("en")
.Campaign("courses-three-months")
.ProfileFromSignedInRecipient())
.WithCard(c => c.Title("Personal access with COURSES3M"))
.Sharing(s => s.ReusableInvitation().AllowRecipientCard())
.Publish(hub, activity);
The result is data: ActionLinkId, RecipientUrl, CardUrl, DefinitionVersion, and a durable ActivityActionDescriptor with caption Open QR card. A generic builder may also support .OpenPage(target).Prefill(...) for navigation-only journeys; side-effecting handlers add a recipient review/confirmation step. Never expose arbitrary scripts, unrestricted redirects, or implicit System execution through .Do(...).
Publish is idempotent for (owner, publisherRequestId). Repeating the same request returns its existing link. A changed definition requires a new revision and a fresh publication request, or an explicit authorized update. Activation binds to a terms snapshot, not whatever a mutable link happens to mean later.
Data and endpoints
Names and paths below are proposed. Use registered content discriminators and owning-hub create/update verbs; these are not hand-authored operational nodes to seed into GitSynced plugin spaces.
| Record | Contents and access |
|---|---|
ActionLinkDefinition |
GUID v4 from a cryptographic generator, site identity, publisher, handler key/version, validated target, public presentation, allowed shared prefill, valid window, definition revision, revoke state; private management record |
ActionLinkPresentation |
Sanitized projection only, backed by an explicit Public Viewer grant without Anonymous; no raw coupon, billing, user, or action-management data |
ActionContinuation |
Expiring opaque handle bound to site and auth transaction, then recipient identity; carries link id/version and validated local return route server-side |
ActionClaim |
Server-owned, recipient-private domain record, link id, coupon identity, terms snapshot, provider setup reference, idempotency key, domain completion and result path; issuer gets permitted aggregate status only |
| Activity action descriptor | Stable handler-independent Navigate result with label and local target; authorization is rechecked when opened |
Only the server writes claim identity, quote, provider references and fulfillment milestones; recipients may submit only validated intent and consent fields. Use a provisioned service-owned operational partition with a documented access policy, not ad hoc nodes inside the GitSynced Essentials or Store partitions. Provision it through supported tenant/service bootstrap. No human Admin grant on a synced space, no global-admin read shortcut. Anonymous middleware can resolve an opaque continuation internally, but cannot read the protected presentation through MCP, search, content, or layout endpoints.
Proposed routes:
https://{site}/a/{guid}— canonical scanned URL. GET is safe and read-only. An anonymous request enters login with an opaque continuation; after authentication it shows the offer.https://{site}/a/{guid}/card— reusable card, also signed-in Public. Card-specific editing controls are visible only with publisher authorization.- A handler-owned command on the recipient's typed claim node — review, setup, activate, retry. Use a POST/command with anti-forgery and authenticated ownership checks, never a GET side effect.
The GUID is an identifier, not a credential or entitlement. URLs, QR images, OG previews, browser history and analytics must not contain personal data, payment secrets, coupon internals, session cookies or bearer grants. Apply noindex, restrictive referrer policy and redacted route telemetry. Invalid, expired and revoked links receive a generic non-enumerating response before sign-in. Public access must be an explicit authenticated Public grant: PartitionAccessPolicy.PublicRead would also expose the page anonymously and is unsuitable. Check ancestor grants and all alternative read surfaces.
Authentication, prefills and sites
- Resolve the link only against the configured origin/site registration. Never trust Host, forwarded headers, a caller-supplied URL, or a query string as authorization to create redirects.
- Store continuation before login and validate it again after callback. Bind it to the auth transaction and destination site; use a short TTL and consume the auth handoff once. Keep the stable link resumable after expiry so the user can restart login safely.
- Preserve it through email verification, onboarding, back navigation and refresh. If the account changes, rebind only after a new authenticated flow; do not attach the first user's setup or claim to the second user.
- Cross-origin journeys start at the destination site and use its normal identity/SSO process. Never transfer session cookies, viewer names as identity proof, or client secrets between sites.
- Shared prefills may contain campaign, locale and a validated public course reference. Name/email come from the current authenticated viewer's own profile and are editable. Never place the sender's personal details in a reusable QR link.
- A recipient-specific invitation is a distinct mode: encrypt/store personal values privately, bind access to the intended verified identity, show no private values to other scanners, and never reuse that invitation as a public phone card.
- Revalidate on submission. Price, plan tier, duration, scope, claim budget, redirect and entitlement fields are not editable prefills. Normalize and size-limit text; reject unknown keys and encode all rendered values.
Execution and durable recovery
Navigation and redemption are separate. The initial GUID page may build an allowed prefilled form but does not create a charge, save a payment method or grant access. The user explicitly accepts method storage and activates the offer.
The claim has domain milestones such as Draft, AwaitingSetup, SetupVerified, Activating, Active, DeliveryPending, Expired, Failed, and Cancelled. Ordinary Activities hold execution status/logs. Do not recreate an Activity control plane on the claim; retain only durable domain milestones and correlation identifiers needed for safe retries.
- All hub/view code is
IObservable<T>. HTTP/provider/file I/O uses the configuredIIoPoolleaf. Noasync, blocking waits or detached tasks above that boundary. - Create a durable claim and idempotency key before the provider call. Reuse them when the browser retries or an activity resumes.
- Only a verified provider event or server reconciliation can satisfy setup; the return page is not evidence. Before activation recheck link revocation, current coupon validity, identity, site and accepted terms.
- Store-domain fulfillment records completed stages. A crash after plan activation but before install resumes delivery without creating another grant or consuming another redemption. Duplicate and out-of-order events converge to the same result.
- If a link is revoked after setup but before activation, do not grant it. Explain that no payment occurred; allow method removal according to the recipient's billing preferences. Do not delete a method they already held for other uses.
- Reopening a completed journey shows the active offer/result for that recipient; it cannot extend the term on every scan. Another recipient gets an independent claim, subject to coupon policy.
- Activity history renders the stored navigation descriptor after process restart, with revoked/unavailable state when appropriate. Do not persist a live UI object graph and assume it can be revived.
Security and review boundaries
This design introduces a public route and payment-method storage. The implementation issues explicitly require maintainer review of the neutral contracts, issuer policy, recipient access policy, and payment-consent behavior before enabling them. The design does not change current security permissions.
Publishing a coupon journey requires domain-specific authority to distribute that coupon, not merely the ability to run an Activity. Public recipients gain only the right to see the sanitized offer; coupon validation authorizes a tightly scoped system write inside the existing domain fulfillment chain. No public handler can select an arbitrary hub, partition, impersonated identity or C# body.
Server-side throttling covers lookup, signup continuation, setup creation and activation; audit mutation attempts without logging payment secrets or personal prefills. Rate limits return actionable retry states. Consent text/version and the terms accepted are stored per claim, including feature selections, method-storage permission and, only if selected, the recurring mandate. Ordinary free-coupon redemption remains governed by the existing coupon policy until a separately approved migration changes it.
Optional features are registered Store-owned capabilities with versioned payload and consent schema, eligibility predicates, explicit defaults and domain handlers. No arbitrary callback or billing option comes from URL prefills. Personal renewal is the first such feature; adding another feature requires a known handler and test coverage. Selecting renewal must never be smuggled into the payment-method consent under the explanation that some catalog items cost money. The feature checkbox starts unchecked; the design may show its checked state after selection. The final CTA repeats the zero-today amount and future price/date. A new price or shortened term invalidates prior acceptance and returns the recipient to review.
Verification and release criteria
| Area | Required evidence |
|---|---|
| Access | Anonymous cannot read card/offer data by any route; a fresh signed-in, unentitled user can see only the sanitized offer; private claims and publisher controls remain denied |
| Authorization | Unauthorized publisher, altered handler/version/site, private target, tampered prefill and open redirect all fail without side effects |
| Authentication | Existing account, new account, email verification, onboarding, expired continuation, changed account, cross-site callback and standalone PWA all resume the correct link |
| Activity durability | Publish once under concurrent retries; historical Open QR card works after kernel unload, process restart and module refresh |
| Execution | GET, crawler preview and scan do not mutate; duplicate submits/webhooks and crash recovery produce one bounded grant and one install |
| Phone | Actual iPhone Safari/home-screen and Android Chrome/home-screen scans, login, install guidance, offline recovery and camera readability pass |
| Payments | No activation charge in either branch; default setup creates no invoice/subscription; opted-in renewal creates only the explicitly authorized zero-due trial and future schedule; separate purchases need fresh confirmation; all available method paths and card fallback are tested |
| Boundaries | Expiry, cap, existing subscriber, failed setup, revoked link, invalid coupon, failed install and unavailable handler have explicit outcomes |
Implement core contracts first through the supported platform CD pipeline, then registry-delivered plugin consumers and clients. Validate node shape, compile all touched types and run their Tests areas on a local mesh; run required PR checks and review. Production verification uses a dedicated authorized test campaign and Stripe test mode before any reviewed live smoke test. Never consume the real COURSES3M offer or charge a real person as an unannounced test.
Decisions still needed before enabling the feature
- Registered destination site(s), first course or catalog landing, publisher audience, and distribution/revocation policy.
- Whether all activation recipients must save a method, or whether a deliberate no-method path is offered. This draft follows the requested method required upfront policy and says so before signup; it must not silently broaden collection to unrelated coupons.
- Whether to keep the current 31 December cap or issue a separately approved offer with a full 90-day term for late recipients. The current cap is preserved in this draft; any optional renewal starts at the precisely displayed end, never at an undisclosed earlier date. If the advertised offer promises a full three months, block publication until its approved terms actually provide that period.
- Approved recurring price/currency/cadence, renewal eligibility, notices and cancellation policy. A checked feature without an authoritative price/date cannot proceed. These business terms are not invented by the design or derived from the coupon name.
- Retention/removal policy for saved methods, consent evidence and personal invitation fields; required legal/payment-provider copy review for the actual launch market.
- Final core contract surface and plugin registration seam, reviewed with the maintainer. There is no authorization here to enable arbitrary public actions or change the live coupon.