Skip to content

Partners

A Partner is the organization that manages one or more tenants on behalf of their customers. In Craft Easy terms, the scope hierarchy runs System → Partner → Tenant: above the tenant sits a partner, and above the partner sits the system operator. Most SaaS deployments use exactly one partner (the vendor itself). White-label resellers use many — one per reseller — each of which owns a slice of the tenant population.

This page describes the Partner model, the routes that manage partners, and how users with scope="partner" interact with the tenants they administer.

When to use partners

Partners exist to answer one question: who is allowed to see across multiple tenants without being a system user?

A system user (scope="system") has unrestricted access. That is too much privilege for a reseller or support organization that should only see its own customers. A partner user (scope="partner") is restricted to the tenants owned by their partner via Tenant.partner_id, and cannot touch anything else.

Typical uses:

  • A software reseller manages 30 customer tenants and issues invoices to all of them.
  • An internal "operations" team owns the provisioning pipeline for a region and needs support access to its tenants but not to others.
  • A holding company bills a group of subsidiary tenants through a shared agreement.

If you only have one operator — you, the platform vendor — you can still create exactly one partner document and assign every tenant to it. That is the default setup for self-hosted deployments.

The Partner model

craft_easy.models.partner.Partner is a system-level document (not tenant-scoped). It holds organizational metadata and nothing more — no users, no permissions, no features. All of those live on the tenants the partner manages.

Field Type Purpose
name string, indexed Display name of the partner organization
slug string, unique, indexed URL-safe identifier, ^[a-z0-9\-]+$, 2-50 chars
description string Optional short description
contact_email string Primary operations contact
contact_phone string Primary operations phone
is_enabled bool Master disable flag — false hides the partner from new tenant assignment
logo_url string Optional logo for white-label admin skins
primary_color string Optional brand color in hex

The model has no cascade rules attached to its own fields. Partner deletion is a soft delete, and Tenant.partner_id has an on_delete="null" cascade — deleting a partner does not delete its tenants, it just nulls their partner_id, leaving them as orphans under system ownership.

Managing partners

All partner routes live under /partners and require scope="system":

GET    /partners                       # list
POST   /partners                       # create
GET    /partners/{partner_id}          # fetch
PATCH  /partners/{partner_id}          # update
DELETE /partners/{partner_id}          # soft delete
GET    /partners/by-slug/{slug}        # fetch by slug
GET    /partners/{partner_id}/tenants  # list tenants managed by this partner

Example — creating a partner and assigning tenants to it:

# 1. Create the partner
curl -X POST https://api.example.com/partners \
  -H "Authorization: Bearer $SYSTEM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Nordic Resellers AB",
    "slug": "nordic-resellers",
    "contact_email": "ops@nordic-resellers.example",
    "primary_color": "#1E40AF"
  }'

# 2. Link a tenant to it
curl -X PATCH https://api.example.com/tenants/65ab... \
  -H "Authorization: Bearer $SYSTEM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: \"5\"" \
  -d '{"partner_id": "65c7..."}'

The tenants-of-partner endpoint

GET /partners/{partner_id}/tenants returns every tenant where partner_id matches. Unlike the two endpoints above, this route is available to users with scope="partner" — but only if the token's partner matches the {partner_id} in the URL. That is the single legitimate cross-tenant read for partner-scope users.

Partner-scope users

A user with scope="partner" has:

  • A partner_id on their token, set at login time from the user's organizational link.
  • Access to every tenant where Tenant.partner_id == user.partner_id for read and, optionally, write operations.
  • No access to tenants owned by other partners or by no partner at all.

Access to a specific tenant still flows through that tenant's access groups — the partner scope only widens the set of tenants the user may enter, not the rights they hold within any one tenant. In practice, most partner users are pre-provisioned with an admin access group in each tenant they manage, either via AccessGroupTemplate inheritance or via explicit data_access grants on each tenant.

When a partner user makes a request, the resolver works like this:

  1. Token says scope="partner", partner_id="65c7...", tenant_id="65ab...".
  2. Scope guard accepts the request if the route requires scope="partner" or lower.
  3. Tenant isolation still rewrites queries to match tenant_id="65ab...". The partner scope does not suppress tenant filtering — it only means the user may enter more than one tenant, not that a single request reads from many.

To look at a different tenant under the same partner, the client must mint a new token scoped to that tenant. The /auth/switch-tenant endpoint (see Sessions & Tokens) does this in one call and validates that the new tenant belongs to the same partner.

Tenant -> Partner resolution

A tenant is linked to a partner via Tenant.partner_id. The tenant also caches partner_name on itself for display purposes, kept in sync by the cascade on_update rule — rename the partner and every linked tenant is updated automatically.

When a parent tenant creates a child via POST /tenants/{id}/sub-tenants, the child inherits partner_id from its parent unless the caller explicitly overrides it. See Organization Hierarchy for how this interacts with feature inheritance.

Setting up white-label branding

Because every tenant carries its own branding block, partners are not the source of branding. A partner is metadata — the actual logo, colors, and custom domain live on each tenant and are resolved per request via GET /branding/resolve?domain=... or ?slug=....

Partners do carry a logo_url and primary_color of their own, used for the partner admin UI (the surface a partner-scope user sees when managing multiple tenants at once). Each tenant under the partner can still override these values with its own branding.

Read Agreements next for how a partner's relationship with a tenant is formalized through TenantAgreement, which captures fees, revenue splits, and feature grants in a signed record.