Designing the Next Layer of PromoBet: Subscription Quotas, PromoGiros and Extensible Capacity

The latest stage of PromoBet development has been less about adding another isolated feature

PromoBetArchitectureNext.jsReactNode.jsFirestoreMulti-TenantServer ComponentsFrontend ArchitectureBackend Architecture

From UserArea Refactoring to the Next Resource Model

The latest stage of PromoBet development has been less about adding another isolated feature and more about making sure the platform is structurally ready for the features that are coming next.

The recent UserArea refactor is a good example of that direction. The frontend now has a clearer separation between the client-side orchestration layer and the presentation sections that consume that state. UserAreaClient centralizes the interactive state and passes the relevant information into more focused UI sections, while Global Catalog and tenant activity can now be treated as distinct experiences.

This work was consolidated in PR #34, Refactor/user area new UI new structure, which was merged on September 7, 2026 after 35 commits. The PR introduced the new UserArea client layer, separated Global Catalog and tenant interactions, reorganized quota and activity UI, and established the foundation for future engagement and subscription-related features.

PromoBet — PR #34: Refactor/user area new UI new structure

The next step is to use that structure to formalize something that already exists implicitly in the backend: not every spin resource is the same kind of resource.

That distinction becomes especially important as PromoBet moves toward subscriptions, promotional spins, and eventually paid tenant capacity.


The Current Quota Model

At the moment, a user's normal spins are derived from their subscription.

The three current user plans are:

text
| Plan     | Global spins | Tenant multiplier | Weekly tenant quota | Monthly tenant quota |
| -------- | -----------: | ----------------: | ------------------: | -------------------: |
| Free     |           10 |                0x |                  25 |                  100 |
| Premium  |           20 |              0.5x |                  50 |                  200 |
| Premium+ |           30 |                1x |                 100 |                  400 |

This creates two related but distinct limits.

The first is the user's normal spin quota.

For a global spin, the subscription directly determines the daily limit.

For a tenant spin, the limit is composed of the tenant's base capacity plus the user's subscription-derived tenant bonus.

Conceptually:

text
Tenant spin quota =
    Tenant subscription capacity
    +
    User subscription bonus

The second restriction is the user's accumulated interaction with tenants.

Tenant usage is tracked independently through weekly and monthly counters. This prevents a user from repeatedly consuming tenant resources without an upper bound, even when their daily spin entitlement would otherwise allow it.

The current spin flow therefore has to satisfy both:

text
User daily spin entitlement
+
Tenant weekly/monthly usage restriction

Only after those checks pass does the spin execute.

This is already a useful separation.

The next step is to make sure we don't accidentally destroy it when introducing additional spins.


PromoGiros Are Not Quota

The first architectural distinction to establish is simple:

A PromoGiro is a source of additional spins, not an increase to the user's renewable quota.

This sounds like a small semantic difference, but it has significant consequences.

A subscription quota is renewable.

For example:

text
Premium

20 normal global spins/day

When the day changes, the normal entitlement becomes available again.

A PromoGiro is different:

text
User receives:

+5 PromoGiros

Those five spins should remain available until consumed.

They should not disappear because the daily spin quota resets.

They should not become part of spinsToday.

They should not increase the user's Premium quota from 20 to 25.

Instead:

text
Subscription
    └── 20 renewable spins

PromoGiros
    └── 5 additional consumable spins

The user therefore has access to 25 possible spins, but the accounting remains:

text
Normal quota:
    20 available

PromoGiro balance:
    5 available

This distinction keeps the two resources independently observable and independently manageable.


PromoGiros Should Be User-Owned

There is another important property of PromoGiros:

They are not tenant-specific.

A PromoGiro should be usable in either of the platform's spin contexts:

text
Global Catalog
       or
Tenant Catalog

That means PromoGiros belong to the user rather than to a particular wallet associated with a tenant.

Conceptually:

text
                    USER

           ┌─────────┴─────────┐
           │                   │
    Subscription           PromoGiros
           │                   │
     Normal spins        Extra spin credits
           │                   │
      ┌────┴────┐          ┌───┴───┐
      │         │          │       │
   Global    Tenant      Global   Tenant

This makes PromoGiros particularly useful as a future promotional mechanism.

They could eventually be granted through different mechanisms without changing the spin engine:

  • onboarding rewards
  • campaigns
  • achievements
  • referral rewards
  • seasonal events
  • administrative grants
  • promotional campaigns
  • eventually, purchases

The spin system only needs to understand that the user possesses an additional consumable spin resource.


PromoGiros Must Bypass Tenant Usage Restrictions

This is probably the most important interaction between the new resource and the existing quota system.

Today, tenant spins are subject to the user's weekly and monthly tenant usage restrictions.

That is intentional.

However, a PromoGiro should be able to provide an alternative path when those normal restrictions have been exhausted.

Consider:

text
Premium user

Weekly tenant quota:
0 remaining

PromoGiros:
3 remaining

A normal tenant spin should be rejected.

But a PromoGiro should still be usable.

Therefore:

text
Normal tenant spin

Check subscription quota

Check tenant weekly/monthly restriction

Consume normal entitlement

while:

text
PromoGiro tenant spin

Check PromoGiro balance

Do NOT consume tenant weekly/monthly allowance

Consume PromoGiro

This gives us a much cleaner model than simply increasing the existing quota counters.

The tenant restriction remains a restriction on normal subscription-funded usage.

PromoGiros provide an independent resource that can intentionally bypass that restriction.


Cooldown Is Different From Quota

The existing spin implementation also has another mechanism that should remain independent: the cooldown.

This distinction is important.

Quota answers:

How many spins can this user consume?

Cooldown answers:

How frequently can this user execute a spin?

Those are not the same question.

PromoGiros should bypass quota restrictions, but they should not bypass the anti-fraud cooldown.

So the eventual model should be:

text
Subscription spin

    ├── Normal quota
    ├── Tenant restriction when applicable
    └── Cooldown

PromoGiro

    ├── PromoGiro balance
    ├── No tenant quota consumption
    └── Cooldown

This preserves the existing protection against rapid repeated requests while allowing promotional resources to serve their intended purpose.


The Spin Needs to Know What Paid or Promotional Resource Funded It

This leads to another useful evolution in the spin domain.

Instead of treating every successful spin as simply:

text
spin = true

the system should eventually know what resource funded that spin.

For example:

text
source: "subscription"

or:

text
source: "promoGiro"

This information becomes valuable for several reasons.

The user history can distinguish normal activity from promotional activity.

Analytics can answer:

text
How many spins came from subscriptions?
How many came from PromoGiros?
How effective was a promotional campaign?

The platform can also eventually distinguish other sources:

text
subscription
promoGiro
reward
admin
purchase
campaign

The important point is that the spin itself remains the common execution mechanism.

Only the resource being consumed changes.


The Same Principle Applies to Tenants

This leads naturally to the second future resource:

ExtendableQuotas.

The tenant side has a different problem.

A tenant has a normal monthly capacity determined by its subscription or plan.

That capacity exists to protect the platform and to make resource consumption predictable.

But some tenants may legitimately have much higher traffic.

For those tenants, an absolute monthly restriction can become counterproductive.

For example:

text
Tenant subscription:

100 monthly capacity

The tenant reaches:

text
100 / 100

At that point, the system could simply stop allowing further normal usage.

But that is not necessarily desirable.

Instead, the tenant could purchase additional capacity:

text
Base monthly capacity:
100

Extended quota:
+500

Effective available capacity:
600

The crucial characteristic is that the extended capacity is not part of the renewable monthly quota.

It is an additional purchased resource.

This is essentially the tenant-side equivalent of PromoGiros.


PromoGiros and ExtendableQuotas Form a Symmetric Model

Once these concepts are separated, the architecture becomes much easier to reason about.

text
USER

├── Subscription
│   └── Renewable spin entitlement

└── PromoGiros
    └── Consumable additional spins


TENANT

├── Subscription
│   └── Renewable capacity

└── ExtendableQuotas
    └── Consumable additional capacity

The difference is the owner and what the resource represents.

User-side

PromoGiro

"This user can execute one additional spin."

Tenant-side

ExtendableQuota

"This tenant can accommodate additional user activity beyond its normal monthly capacity."

Both are additional resources.

Neither should redefine the underlying subscription.


Why This Matters Before Stripe

At first glance, these concepts might appear to be payment features.

Eventually they will be.

But they shouldn't be implemented as payment features.

The payment provider should only be responsible for establishing that a purchase or subscription occurred.

The application should then translate that event into an internal resource.

Conceptually:

text
Payment

Stripe

Webhook

Application

Grant resource

For example:

text
Premium subscription purchased

subscription = premium

or:

text
PromoGiro package purchased

promoGiros += 10

or:

text
Tenant capacity package purchased

extendableQuota += 500

The spin engine should never need to ask Stripe:

text
"Can this user spin?"

It should only ask the application's own domain model:

text
"What resources does this user have available?"

This keeps the payment integration replaceable and prevents billing concerns from leaking into the core game mechanics.


The Frontend Refactor Is Part of This Architecture

This is also why the recent UserArea refactor is more important than it might initially appear.

The new structure gives us a dedicated client orchestration layer while allowing individual sections to remain focused on presentation and interaction.

The merged PR introduced UserAreaClient, reorganized the UserArea into dedicated sections, separated Global Catalog and tenant activity, and introduced UI foundations for quotas and PromoGiros.

That means we now have somewhere appropriate to expose these new concepts without making the entire UserArea aware of their implementation details.

For example:

text
UserAreaClient

      ├── Subscription

      ├── Remaining Quota

      ├── PromoGiros

      ├── Tenant Activity

      ├── Global Activity

      └── Trophies

Each section can evolve independently as the backend resource model becomes more complete.


Preparing the Next Server/Client Boundary

There is also another architectural benefit to the current direction.

The goal isn't to make every page a Client Component simply because some part of the page needs interactivity.

Instead, the structure now gives us room to move toward a more deliberate Server Component boundary.

The eventual direction can be:

text
Server Component

    ├── authentication/session
    ├── initial data
    ├── metadata
    ├── SSR concerns

    └── Client Component

          ├── interaction
          ├── local state
          ├── animations
          └── browser-only behavior

That becomes particularly useful as PromoBet gains more page-level capabilities.

For example, future pages can potentially take advantage of server-side metadata generation, better initial rendering, and server-side data preparation without forcing the entire page into a client-side execution model.

The important architectural decision is therefore not simply "use Server Components."

It is:

Keep the server/client boundary intentional so that interactive behavior exists where it is needed without making the entire route interactive by default.

The current UserAreaClient structure gives us a practical boundary for continuing in that direction.


The Next Implementation Step

With the architecture established, the next implementation should not begin with Stripe.

It should begin with the internal resource model.

The sequence is better represented as:

text
1. Define PromoGiro resource

2. Add PromoGiro accounting

3. Teach spin how to consume PromoGiros

4. Preserve cooldown and tracking

5. Verify Global + Tenant behavior

6. Expose PromoGiro state in UserArea

7. Define Tenant ExtendableQuota

8. Integrate payment later

This allows the entire user/tenant experience to be exercised before introducing the complexity of payment processing.

That is especially useful because the platform can now validate the interaction between:

text
User
Tenant
Subscription
Global Catalog
Tenant Catalog
Normal Quotas
Tenant Restrictions
PromoGiros
Cooldowns
History
Statistics

without Stripe becoming a dependency of the development process.


Proposed Branch Separation

This also gives the upcoming work a natural separation.

The frontend work can evolve around:

text
PromoGiro UI
User subscription state
Remaining resources
UserArea presentation
Spin source visibility

while the backend work can focus on:

text
PromoGiro domain
PromoGiro accounting
Spin resource consumption
Quota resolution
Tenant restrictions
Future ExtendableQuota domain

The separation mirrors the architecture already being established rather than creating another cross-cutting feature branch.


The Larger Picture

What initially looks like a subscription system is becoming something slightly more interesting.

PromoBet is moving toward a resource-based model where subscriptions establish renewable entitlements, while additional purchases and rewards provide independent consumable resources.

That distinction gives the platform much more flexibility.

A subscription can define:

text
"What does this user normally get?"

A PromoGiro can define:

text
"What additional activity has this user been granted?"

A tenant subscription can define:

text
"What level of platform capacity does this tenant normally have?"

And an ExtendableQuota can define:

text
"How much additional capacity has this tenant acquired?"

The game engine then becomes the common consumer of these resources rather than the place where subscription, promotion, and payment logic are mixed together.

That is the direction worth establishing before implementing the next branches.

The objective of the next phase is therefore not yet to build payments.

It is to make sure that the user and tenant experiences behave correctly when different types of resources coexist.

Once that model is stable, Stripe becomes an integration layer on top of an already well-defined domain rather than something that dictates how the domain itself works.


Next Milestone

The immediate milestone is therefore:

Implement PromoGiros as an independent, user-owned consumable spin resource.

They should:

  • work for both Global Catalog and tenant spins;
  • survive the normal daily quota reset;
  • not increase or modify the renewable subscription quota;
  • not consume the user's weekly/monthly tenant restriction;
  • still respect the normal spin cooldown;
  • be recorded as a distinct spin source;
  • be visible through the UserArea;

After that behavior is proven, the same principles can be applied to tenant ExtendableQuotas.

Only then does it make sense to connect these resources to actual payment flows.

The architectural goal is simple:

Subscriptions define renewable access. Promotional and purchased resources provide additional capacity. The spin engine consumes resources without needing to know where they came from.

That separation gives PromoBet room to grow without turning the core game flow into a payment system.