YabbaDeck.Platform.Contracts
1.3.0
dotnet add package YabbaDeck.Platform.Contracts --version 1.3.0
NuGet\Install-Package YabbaDeck.Platform.Contracts -Version 1.3.0
<PackageReference Include="YabbaDeck.Platform.Contracts" Version="1.3.0" />
<PackageVersion Include="YabbaDeck.Platform.Contracts" Version="1.3.0" />
<PackageReference Include="YabbaDeck.Platform.Contracts" />
paket add YabbaDeck.Platform.Contracts --version 1.3.0
#r "nuget: YabbaDeck.Platform.Contracts, 1.3.0"
#:package YabbaDeck.Platform.Contracts@1.3.0
#addin nuget:?package=YabbaDeck.Platform.Contracts&version=1.3.0
#tool nuget:?package=YabbaDeck.Platform.Contracts&version=1.3.0
YabbaDeck.Platform.Contracts
The public message contracts of the YabbaDeck app platform — the events an app backend consumes to stay in step with the platform that owns its users and takes its money: the user lifecycle, and since 1.1.0 payments and entitlements.
Dependency-free by design. This package has no dependencies at all, and never will: it is restored by every app backend in the ecosystem, and anything pulled in here becomes a version conflict somebody else has to resolve.
You probably want the AppBackend packages instead
This package is the wire format. If you are building an app backend on .NET, take
YabbaDeck.AppBackend — it consumes every event
below, keeps the mirror rows, and gives you the entitlement gate and the reconciliation read, all with
the rules on this page already applied. Add
YabbaDeck.AppBackend.AspNetCore for
the token validation, the client meta-headers and the health endpoints.
modelBuilder.ApplyYabbaDeckPlatform(); // your DbContext
services.AddYabbaDeckAppBackend<MyDbContext>(configuration); // mirrors, gate, reconciliation
services.AddMassTransit(bus => bus.AddYabbaDeckConsumers()); // on your own bus
Read on if you are writing a consumer by hand — because you are on another stack, another language, or you want to know exactly what those packages do.
The events
All four carry the same three values and live in YabbaDeck.Platform.Contracts.Users:
| Event | Published when |
|---|---|
UserVerified |
Someone followed the verification link in their mail and the account became active. |
UserDisabled |
An operator suspended the account. Reversible. |
UserEnabled |
A suspension was lifted. |
UserRemoved |
The account was hard-deleted (right to erasure). Delete your mirror row. |
public sealed record UserVerified(string AppId, Guid UserId, DateTimeOffset OccurredAt)
: IUserLifecycleEvent;
AppIdis the app's public id — the slug your backend knows itself by from its own configuration (the same value clients send asX-App-Id), never a YabbaDeck primary key.UserIdis the shared GUID. YabbaDeck mints it; your mirror row uses it as its own primary key. Never generate one on your side — supplied or nothing.IUserLifecycleEventcarries the three common values, so one filter can cover the whole set.
The payment events
Since 1.1.0, in YabbaDeck.Platform.Contracts.Payments:
| Event | Published when |
|---|---|
PaymentCompleted |
Money was captured for one of your users. |
EntitlementGranted |
Somebody is now entitled to something — this is usually the one to act on. |
IPaymentEvent carries the two common values (AppId, OccurredAt) so one filter can cover the set.
- Nothing here names a payment provider. No provider name, id or status — every value is YabbaDeck's own. The moment this package named a PSP, every backend in the ecosystem would be coupled to the platform's choice of one.
ProductIdis your string, echoed back exactly as your checkout named it. The platform holds no catalogue and never interprets it.- Money is minor units —
1050is 10.50 kr. Never a decimal. EntitlementGranted.ExpiresAtis nullable, andnullmeans perpetual. Write your mirror row against a nullable date from the start: treating it as a boolean is the shortcut that becomes a migration the first time somebody sells a subscription.- Key your mirror row on
EntitlementId, and gate on whether the user holds any active entitlement for the product. A redelivery then finds the row already there, and a renewal arrives as the same id with a later expiry, because the platform moves the date on the row rather than granting a second one. Not on (AppId,UserId,ProductId) — two separate purchases of one product are two grants with two ids, and collapsing them onto one row loses one of them. - Applying a grant only ever widens access. A redelivery changes nothing; a later expiry extends; an
older delivery arriving late must not shorten what somebody already has; and
nulloutranks every date and is never replaced by one. Delivery is at-least-once and unordered — this is what makes that harmless. - The event is a cue, not the authority. A broker promises at-least-once, which is a promise about
retries, not about a message that was never published because something was down when it mattered. So
before you tell somebody they own nothing, ask the platform:
GET /api/v1/users/{userId}/entitlementswith your backend's own machine token (entitlements:read). A dropped event then costs a moment of staleness rather than a customer's purchase.YabbaDeck.AppBackenddoes this for you.
The subscription events
Since 1.3.0, in the same namespace. A subscription is billed by the payment provider on its own schedule, and the platform tells you what happened:
| Event | Published when |
|---|---|
SubscriptionStarted |
The first period was paid for. Carries the SubscriptionId your backend cancels or changes it by. |
SubscriptionRenewed |
Another period was paid for, as a renewal or as a failed one the provider recovered. |
SubscriptionPaymentFailed |
Charging for the next period failed and the provider is retrying. Access is not taken away. |
SubscriptionCanceled |
Somebody canceled it. Immediately says whether access ended now or runs to the end of the paid period. |
SubscriptionEnded |
It stopped billing for good. Reason is one of SubscriptionEndReasons. |
- Your entitlement mirror needs none of these. Access still arrives as
EntitlementGranted. A renewal republishes it with the sameEntitlementIdand a laterExpiresAt, and the rules above already apply that correctly. A lapse needs no event: the entitlement just expires. An immediate cancellation arrives asEntitlementRevoked. - Every one carries
AccessUntil, when access runs out: the end of the last paid period plus any grace period the platform gives. Use it to tell a user where they stand; gate on the entitlement. - Every billed period is its own payment.
PaymentCompletedis published for each successful charge, renewals included, with thePaymentIdthe subscription event names. - Nothing is refunded by a cancellation. Money going back is its own act, with its own event.
Consuming them by hand
(YabbaDeck.AppBackend already does all of this — see the top of this page.)
Publishing is MassTransit fan-out: every app backend receives every event, and each consumer drops what is not addressed to it. That is what lets a generated backend work unmodified, with no per-app routing to configure.
public sealed class UserVerifiedConsumer(IOptions<AppOptions> app, IUserMirror users)
: IConsumer<UserVerified>
{
public async Task Consume(ConsumeContext<UserVerified> context)
{
// Not ours — another app on the same broker. Drop it. Ordinal: a public id is a slug, and
// this comparison is the only thing standing between your app and another app's data.
if (!string.Equals(context.Message.AppId, app.Value.PublicId, StringComparison.Ordinal))
{
return;
}
await users.EnsureAsync(context.Message.UserId, context.CancellationToken);
}
}
Make consumers idempotent. The same event may be delivered more than once, and UserVerified
for an id you already hold must be a no-op. The event is the sync path, not the only one: a
protected endpoint should also upsert the local user from the access token's sub on first use, so
a dropped or delayed message costs a moment of staleness rather than wedging the app.
Versioning
The version is explicit in the project file and bumped deliberately. Adding an event is a minor bump; changing the shape of an existing one is a major. Pin the version you build against.
| 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
- No dependencies.
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 |
|---|