YabbaDeck.AppBackend
1.4.0
dotnet add package YabbaDeck.AppBackend --version 1.4.0
NuGet\Install-Package YabbaDeck.AppBackend -Version 1.4.0
<PackageReference Include="YabbaDeck.AppBackend" Version="1.4.0" />
<PackageVersion Include="YabbaDeck.AppBackend" Version="1.4.0" />
<PackageReference Include="YabbaDeck.AppBackend" />
paket add YabbaDeck.AppBackend --version 1.4.0
#r "nuget: YabbaDeck.AppBackend, 1.4.0"
#:package YabbaDeck.AppBackend@1.4.0
#addin nuget:?package=YabbaDeck.AppBackend&version=1.4.0
#tool nuget:?package=YabbaDeck.AppBackend&version=1.4.0
YabbaDeck.AppBackend
The platform half of an app backend on the YabbaDeck platform: the mirror rows for the platform's users and entitlements, the consumers that keep them in step, the entitlement gate an app protects its own features with, and the reconciliation read that makes a dropped event a moment of staleness rather than a lost purchase.
Pair it with YabbaDeck.AppBackend.AspNetCore in a web host. This half has no ASP.NET Core
dependency, so a worker or a message handler can take it alone.
Why it is a package
Every service generated from YabbaDappBackendTemplate used to carry this code as a copy. A fix made
here had to be made again by hand in each of them, by somebody who knew to — and a service generated
before a feature existed never got it at all. A version bump is what this package buys.
What it gives you
User |
The mirror row. Three fields, and the id is the platform's — supplied, never generated. It is the hook your app hangs its own columns and foreign keys on. |
Entitlement |
What somebody bought. ExpiresAt is nullable and null means perpetual — never store this as a boolean. |
IUserSyncService |
Keeps the mirror user in step. Used by the lifecycle consumers and by your protected endpoints, so both have one idea of what idempotent means. |
IEntitlementService |
The gate. HasActiveAsync(userId, productId) is what an app checks before unlocking a feature. |
IPlatformEntitlementsClient |
The reconciliation read. null means "the platform did not answer" — never an empty list. |
IPlatformCheckoutClient |
Opens a checkout, which a client cannot do — the request carries the price. |
IPlatformSubscriptionsClient |
Starts, cancels and re-plans a subscription (1.3.0). Renewals need nothing from you: they arrive as the same EntitlementGranted, extended. |
| Four consumers | UserVerified, UserRemoved, EntitlementGranted, PaymentCompleted. Each drops what is not addressed to this app. |
Wiring it up
// 1. Your DbContext takes the platform's tables. They stay in YOUR model, so your own tables can
// hold a foreign key to User — which is the whole reason that row exists.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyYabbaDeckPlatform();
// ... your own configurations
}
// 2. Registration. The type parameter is your context.
services.AddYabbaDeckAppBackend<MyDbContext>(configuration);
// 3. The consumers go on YOUR bus, beside your own, so you keep the endpoint name formatter.
services.AddMassTransit(bus =>
{
bus.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter(prefix: "my-service", includeNamespace: false));
bus.AddYabbaDeckConsumers();
bus.UsingRabbitMq((context, cfg) => { /* ... */ cfg.ConfigureEndpoints(context); });
});
The queue-name prefix is not cosmetic. Every app backend on the platform shares one RabbitMQ
virtual host, because that is the only way the platform's events reach any of them. Two services
declaring a plain user-verified queue would be competing consumers on one queue — each event
reaching exactly one of them, silently, looking for all the world like a flaky broker.
Opening a checkout
Your backend is the seller, so your backend opens the sale:
var checkout = await platformCheckout.OpenAsync(new PlatformCheckoutRequest(
UserId: userId,
ProductId: "premium",
AmountMinor: 4900, // 49.00 kr — minor units, never a decimal
Currency: "SEK",
ReturnUrl: $"myapp://payments/return?order={orderId}", // YOUR reference — see below
CancelUrl: "myapp://payments/cancel",
IdempotencyKey: orderId)); // a double-tapped button is one payment
The price is in the request, and that is the whole reason this call exists here rather than in your app. The platform holds no catalogue and cannot derive an amount from a product string, so the seller states it — over a machine credential the customer never sees. A client naming its own price is a free-money bug.
The catalogue stays yours. This package takes an amount and sends it; what anything costs is your decision, and a package that shipped a catalogue would be a package with an opinion about your business.
The return URL cannot carry the platform's payment id. The id is minted while your request is being handled, so it does not exist when you build the URL — and the platform then redirects there verbatim, appending nothing of its own.
Usually nothing needs it to. PaymentId comes back from this call before the customer ever leaves
for the hosted page, so hand it to your client and let it watch; the return link only has to mean "they
are back". When you do need the payment named in the link — a cold start after the app was killed
mid-payment — either put your own reference in the URL and map it afterwards, or point the URL at
your own backend, which knows the mapping by then and can redirect on to your deep link with the
payment id in it.
Failures are a PlatformCheckoutException whose Reason distinguishes the four things that actually
go wrong — no credential, a credential not granted payments:checkout, a request the platform will not
take, and no gateway selected for the app — because each is fixed by a different person.
It needs payments:checkout on top of entitlements:read. The token request asks for no scope
and takes what your client is granted: asking for both would fail outright in a deployment granted only
one, and take entitlement reconciliation down with it. A missing grant shows up as a clear failure on
the call that needed it.
Subscriptions
Since 1.3.0. The payment provider bills every renewal on its own schedule; your backend starts the subscription, and can cancel it or move it to another plan:
var checkout = await platformSubscriptions.OpenAsync(new PlatformSubscriptionRequest(
UserId: userId, // required: a subscription keeps somebody's access alive
ProductId: "premium-monthly",
AmountMinor: 4900, // per period, in minor units — your price, as at checkout
Currency: "SEK",
Interval: PlatformBillingInterval.Month,
IntervalCount: 1,
ReturnUrl: "myapp://subscriptions/return",
CancelUrl: "myapp://subscriptions/cancel",
IdempotencyKey: orderId));
// Keep checkout.SubscriptionId — it is what you cancel or change it by, for the user it belongs to.
await platformSubscriptions.CancelAsync(subscriptionId, userId); // at the end of the paid period
await platformSubscriptions.CancelAsync(subscriptionId, userId, atPeriodEnd: false); // now — access ends at once
await platformSubscriptions.ChangePlanAsync(subscriptionId, userId,
new PlatformSubscriptionPlan("premium-yearly", 49000, "SEK", PlatformBillingInterval.Year));
userId is the user your backend is acting for: from their token, never from the request (1.4.0). The
platform touches only a subscription that belongs to them, and anybody else's fails with NotFound, as one
that does not exist does. A subscription id reaches you from a client, and nothing in your database says
whose it is, so this is what stops one of your users canceling another's. 1.3.0's calls took no user.
Your entitlement mirror already does the rest. Access is an entitlement like any other. A paid renewal
republishes EntitlementGranted with the same id and a later expiry; a failed renewal extends
nothing, so access lapses at the end of the last paid period without any event; an immediate
cancellation arrives as EntitlementRevoked. The subscription events in YabbaDeck.Platform.Contracts
1.3.0 are there for what else a renewal means to you, not for gating.
Nothing is refunded by a cancellation. A plan change takes effect from the next period, with no proration; if the product changes, the old one's access runs to the end of what was paid for and the new one is granted once its first period is paid.
Canceling and changing need subscriptions:manage on top of payments:checkout, because they take
access away. A deployment granted only the first gets a NotPermitted failure that names the scope, with
everything else working. Failures are a PlatformSubscriptionException whose Reason tells a missing
credential, a missing grant, a rejected request, an app that cannot take payments, an unknown
subscription and one that is no longer live apart.
The migrations are yours
The entities are mapped into your context, so dotnet ef migrations add runs in your project against
your database. A version of this package that changes the model costs you a migration — visible, in
your own diff, on your own schedule. That is the price of the foreign key, and it is the right way
round.
Configuration
{
// Which app this is on the platform, and which platform. No secret: the platform signs tokens with
// RS256 and publishes the public half, so onboarding a backend means handing out nothing.
"YabbaDeckAuth": {
"AppId": "my-app",
"Authority": "https://yabbadeck.example.com",
"Issuer": "yabbadeck-bff"
},
// Optional. Empty means no reconciliation and no checkout — the events are then the only way an
// entitlement arrives. Configuring one of the two and not the other fails startup naming the key.
// The platform issues these: YabbaProv does it when it provisions your app, or an administrator
// does it in the Back Office (PUT api/app-backend-client). The secret is shown once — store it
// then. A lost one is replaced by issuing again, which rotates it; the client id never changes.
"YabbaDeckClient": {
"ClientId": "",
"ClientSecret": ""
}
}
Two properties worth keeping
nullfrom the platform client is not an empty list. Null means it did not answer — not configured, unreachable, refused, or asked again too soon — and your mirror is left as it was. Collapsing the two is how an outage becomes every customer losing what they bought.- The gate re-reads only when it is about to say no. A yes is already the platform's own answer. The consequence is that a revocation reaches the gate on the next list or when the row lapses; nothing on the wire revokes an entitlement yet.
MIT licensed. Part of the YabbaDeck platform.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MassTransit (>= 8.5.10)
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.10)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Http (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Options (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- YabbaDeck.Platform.Contracts (>= 1.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|