Reseller mode (B2B-of-B2B-of-B2B)
Build SaaS / PaaS where your customers can resell your app to their own end-customers. WolfieAuth provides the primitives + SDKs.
Reseller mode — building B2B-of-B2B with WolfieAuth
TL;DR: any app registered with WolfieAuth can opt-in to reseller mode. Once enabled, the app’s owner-org admin can mark customer-orgs as resellers — they get a seat-pack, a dedicated signup link (and optionally a custom domain), and a panel to provision their own end-customer orgs. Three-level fee policy splits revenue between the platform (Wolfie), the app owner-org, and the reseller. SDKs in every framework expose
isResellerOf(),useReseller(), drop-in components, and a typed API client so the app’s own admin UI shows up in 5 lines.
This page walks through how the model works and how to plug it into your app.
The hierarchy
WolfieAuth Platform (Wolfie)
│
│ Stripe Connect onboarding
│ + 5% platform-fee on reseller-subscriptions
▼
┌──── Owner-org of the OidcClient ────┐
│ (your app, e.g. acme-cloud)│
│ │
│ Sells seat-packs to resellers │
│ (Stripe Connect on owner-org) │
│ │
└──┬──────────────────────────────┬───┘
│ │
▼ ▼
Reseller-org A Reseller-org B
(Big Reseller Polska) (some other partner)
│ │
│ Distributes seats to │
│ end-customers (out-of-band │
│ invoicing — wolfieauth │
│ doesn't pay reseller→client │
│ transactions) │
▼ ▼
┌─ Bob's Pizza GmbH ┌─ Other end-customer
├─ Tom's Café └─ ...
└─ Foo's Bar Sp. z o.o.
Three actors, three responsibilities
| Actor | Owns | Decides |
|---|---|---|
| Platform (Wolfie) | WolfieAuth itself | Default platform-fee BPS/flat for ALL reseller-subscriptions; per-app and per-reseller overrides; emergency suspend |
| Owner-org of the app | OidcClient (acme-cloud-product) | Whether to enable reseller-mode at all; which customer-orgs become resellers; seat-limits; per-reseller fees; signup-tokens; custom domains |
| Reseller-org | Their reseller-subscription + their child-orgs | Which end-customers to provision (within the seat limit); when to suspend a customer; whether to charge them out-of-band |
Three-level fee policy
WolfieAuth resolves a per-payment fee through this hierarchy (most-specific wins):
- Per-resellership override (
AppReseller.platformFeeBpsOverride) — Wolfie negotiated a special rate with this specific reseller. - Per-app override (
OidcClient.platformResellerFeeBpsOverride) — Wolfie negotiated with the owner-org (e.g. Acme Cloud has 7% instead of 5%). - Platform default (
PlatformConfig.platformResellerFeeBps) — Wolfie’s global default, 500 bps (5%) out-of-the-box.
The owner-org’s own fee on the seat-pack subscription (the price they charge the reseller) sits separately on OidcClient.resellerFeeBps / AppReseller.feeBps. Both fees apply additively — reseller pays seat-pack price (= owner-org revenue) + a small platform-fee slice that goes to Wolfie.
Setup checklist (owner-org admin)
- Open
/admin/clients/<your-app-id>→ Overview tab. - Toggle 🤝 Tryb reseller / Reseller mode. Save.
- A new Resellers tab appears.
- Set the default
resellerFeeBps/resellerFeeFlatCents(or leave 0 for “free seat-packs”). - Click + Add reseller, pick a customer-org from the dropdown, set their seat-limit. A signup-token is generated and shown once — copy it.
- (Optional) Set a custom domain on that reseller (e.g.
cloud.acme.com). Add the TXT record shown, click ✓ Verify, done. - Hand the signup-token (or the custom-domain URL) to the reseller.
What changes for the reseller’s end-customers
A user signing up via:
https://auth.wolfieguard.com/signup?client_id=acme-cloud-product&reseller_token=<token>
…or via a verified custom domain:
https://cloud.acme.com/signup?client_id=acme-cloud-product
…gets a fresh Organization row with parentId = <reseller's orgId>. Their OIDC tokens carry:
wolfieauth_org_parentId = "<reseller-orgId>"(apps recognize this end-customer is “under” a reseller)wolfieauth_org_id= the new child-org’s id (their own data scope)
The reseller’s admins log in to the owner-org’s admin UI (your app, e.g. Acme Cloud’s admin panel — built using SDK helpers below). Their tokens carry:
wolfieauth_reseller_apps[]=[{ client_id: "acme-cloud-product", org_id: "<reseller-orgId>", seat_limit: 100, seats_allocated: 17, status: "ACTIVE" }]
That’s the gating mechanism — your app sees the claim and shows the Reseller portfolio panel.
Wiring the panel in your app — per SDK
React / Next.js
import { RequireResellerAdmin, ResellerChildrenTable, useReseller } from "@wolfieauth/sdk-react";
// Drop-in (works out of the box):
<RequireResellerAdmin clientId="acme-cloud-product"
fallback={<p>This page is for reseller admins only.</p>}>
<ResellerChildrenTable clientId="acme-cloud-product" />
</RequireResellerAdmin>
// Or build your own UI from the hook:
function MyResellerDashboard() {
const { isReseller, resellership, children, billing,
provisionChild, suspendChild } = useReseller("acme-cloud-product");
if (!isReseller) return null;
return <div>{resellership.seats_allocated}/{resellership.seat_limit} seats — …</div>;
}
The hook expects your app to mount a proxy at /api/wolfieauth/resellers/... that forwards to auth.wolfieguard.com/api/admin/resellers/... with the user’s OIDC bearer token. Your SDK middleware (handle.ts in SvelteKit, middleware in Next.js) typically already has this pattern — see your stack-specific guide.
SvelteKit
// +page.server.ts
import { loadResellerContext } from "@wolfieauth/sdk-sveltekit/server";
export const load = async ({ locals, fetch }) => {
const reseller = await loadResellerContext({
claims: locals.session?.claims ?? null,
clientId: "acme-cloud-product",
fetch,
});
return { reseller };
};
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
{#if data.reseller.isReseller}
<h1>Your customers ({data.reseller.children.length})</h1>
{#each data.reseller.children as c}
<div>{c.name} — {c._count.members} members</div>
{/each}
{/if}
Laravel / PHP
use WolfieAuth\Sdk\Resellers\Resellers;
public function dashboard(Request $request)
{
$claims = $request->attributes->get('wolfieauth')['claims'] ?? null;
if (!Resellers::isResellerOf($claims, 'acme-cloud-product')) {
abort(403);
}
$seats = Resellers::getResellerSeatStatus($claims, 'acme-cloud-product');
return view('reseller.dashboard', compact('seats'));
}
In Blade:
@if(\WolfieAuth\Sdk\Resellers\Resellers::isResellerOf($wolfieauth['claims'], 'acme-cloud-product'))
<a href="/reseller">Manage your customers</a>
@endif
Django / DRF
from wolfieauth.resellers import IsResellerAdmin, is_reseller_of, wolfieauth_reseller_admin
# DRF
class ResellerDashboard(APIView):
permission_classes = [IsResellerAdmin("acme-cloud-product")]
def get(self, request):
...
# Plain Django
@wolfieauth_reseller_admin("acme-cloud-product")
def reseller_dashboard(request):
...
# In a template
{% if request.wolfieauth_claims|is_reseller_of:"acme-cloud-product" %}
<a href="/reseller">Manage your customers</a>
{% endif %}
Building a “PaaS-of-PaaS” with WolfieAuth
Now that the primitives are in place, the wider pattern is: anything you build on top of WolfieAuth can be a multi-tier SaaS. Examples:
- WolfieCloud (managed hosting): Wolfie runs the platform; resellers are agencies that bundle hosting for their clients; end-customers are the agency’s clients (each gets their own org with their own database, mailboxes, etc.)
- Industry-specific CRM (e.g. dentist-CRM-as-a-service): Wolfie runs WolfieAuth; the platform owner-org sells a “Dental Network” seat-pack to large network-operators; each network has a reseller-status that lets them spawn an org per practice; each practice’s staff log in via a per-network branded subdomain.
- White-label e-learning: Same pattern — corporate trainers buy seat-packs, distribute access to their internal “students” each in their own orgs.
The combination of:
- per-OidcClient theme + reseller-scoped
defaultChildThemecascade, - per-org Stripe Connect for the owner-org,
- per-resellership signup tokens + optional custom domain,
- claims-based gating in the SDK,
means the platform owner doesn’t have to write three layers of code to support B2B-of-B2B-of-B2C — WolfieAuth handles identity, branding, fees, seats, and lifecycle. Your app just owns its own product.
API reference
All endpoints under /api/admin/resellers/... and /api/admin/clients/:id/resellers/... — see the REST API doc for full routes, query params, and response shapes.
Migration / rollout
- Existing apps: opt-in.
OidcClient.resellerModedefaults tofalse; existing customer-orgs continue to work unchanged. - Switching it on later is non-destructive: enabling reseller-mode doesn’t change anything about already-onboarded users; they remain root-level orgs.
- Custom domains are optional: an app can use the standard
auth.wolfieguard.com/signup?reseller_token=...link forever and never set up a custom domain. - Three fee policies are independent: the platform-fee + owner-org-fee + per-reseller override all stack additively. Setting any to 0 is supported (= no fee at that level).
Limits and caveats
- Sub-resellers (3+ levels deep) are gated behind
AppReseller.allowSubResellersand only SUPER_ADMIN of the platform can enable. Most cases are flat: platform → reseller → end-customer. - Each reseller-org needs its own Stripe Connect account if it wants to invoice its own end-customers via WolfieAuth — but reseller→end-customer transactions are out-of-scope for WolfieAuth’s billing (the reseller invoices their customers themselves, e.g. via their own wolfiecrm). WolfieAuth tracks only the seat-pack subscription (reseller → owner-org).
- KSeF invoicing for reseller→end-customer is the reseller’s responsibility — their wolfiecrm or external accounting.
Last updated: