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_idon their token, set at login time from the user's organizational link. - Access to every tenant where
Tenant.partner_id == user.partner_idfor 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:
- Token says
scope="partner",partner_id="65c7...",tenant_id="65ab...". - Scope guard accepts the request if the route requires
scope="partner"or lower. - 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.