Skip to content

Partner Management

Partners are the top-level tenants in Breeze’s multi-tenant hierarchy. A partner typically represents a Managed Service Provider (MSP), an enterprise IT department, or an internal team. Partners own organizations, which in turn contain sites, device groups, and devices. Breeze supports both administrator-provisioned partners (created by system-scoped users) and self-service partner registration (where MSPs sign up directly through the web interface).

Partner management encompasses the full lifecycle: registration, configuration, user association, organization management, and feature flag controls. This reference covers the self-registration flow, the partner hierarchy model, administrative operations, and the environment variables that govern partner-related features.


Breeze organizes all managed infrastructure into a strict tenant hierarchy. Partners sit at the top:

Partner (MSP)
+-- Organization (Customer)
+-- Site (Location)
+-- Device Group
+-- Device

Every API query automatically scopes data to the caller’s position in this hierarchy. A partner-scoped user can only access organizations they have been granted access to. Organization-scoped users can only see their own organization and its sites. System-scoped users have unrestricted access across all partners.

TypeDescription
mspManaged Service Provider managing multiple customer organizations. This is the default type assigned during self-registration.
enterpriseA single enterprise managing its own infrastructure.
internalInternal IT team or development environment.
PlanDescription
freeFree tier with default limits. Assigned during self-registration.
starterStarter tier for initial paid onboarding.
communityCommunity tier with basic paid features.
proProfessional tier with expanded limits.
enterpriseEnterprise tier with advanced features.
unlimitedNo enforced limits.
StatusDescription
pendingNewly registered partner awaiting payment or admin approval. Blocked from API access by the partner guard middleware (403 with PARTNER_INACTIVE code).
activeFully operational partner with API access.
suspendedTemporarily suspended (e.g., payment failure). Blocked from API access.
churnedPermanently inactive partner. Blocked from API access.

Partner users are associated with their partner through the partner_users table. Each association includes an orgAccess field that controls which organizations the user can see:

Access LevelBehavior
allUser can access every organization under the partner. This is the default for the admin user created during self-registration.
selectedUser can only access organizations whose IDs are listed in the orgIds array on the partner_users record.
noneUser cannot access any organizations. Useful for partner-level users who only manage partner settings.

Partner self-registration allows MSPs and IT companies to create their own Breeze account without administrator intervention. The registration flow creates a partner, an admin role, a user account, and links them together in a single operation.

  1. User visits /register-partner — The frontend checks the ENABLE_REGISTRATION feature flag. If registration is disabled, the user is redirected to /login?reason=registration-disabled.

  2. User fills out the registration form — Required fields are company name (min 2 characters), full name, email address, password (min 8 characters), password confirmation, and terms of service acceptance.

  3. Frontend submits to POST /auth/register-partner — The API validates the request body against the registerPartnerSchema Zod schema.

  4. Rate limit check — The API enforces a limit of 3 registration attempts per IP address per hour using the Redis-backed sliding window rate limiter. If the limit is exceeded, a 429 response is returned.

  5. Password strength validation — The password is checked against the server-side strength rules. If it fails, a 400 response with the first error message is returned.

  6. Email uniqueness check — The API checks if a user with the same email already exists. If so, it returns a generic success message to prevent email enumeration: "If registration can proceed, you will receive next steps shortly."

  7. Slug generation — A URL-safe slug is derived from the company name by lowercasing, replacing non-alphanumeric characters with hyphens, and trimming to 50 characters. If the slug already exists, a numeric suffix is appended (up to 100 attempts).

  8. Transaction: create partner, role, user, and association — The API creates four records:

    • A partner with type msp and plan free
    • A Partner Admin role with scope: partner and isSystem: true
    • A user with the hashed password and status: active
    • A partner_users association with orgAccess: all
  9. JWT tokens issued — Access and refresh tokens are created with scope: partner and the new partner ID. The refresh token is set as an HTTP-only cookie.

  10. Redirect to dashboard — The frontend stores the tokens and redirects the authenticated user. If this is the first-ever user in the system, the setup wizard may appear.

FieldTypeValidationDescription
companyNamestringmin 2, max 255 charsThe partner/company display name
namestringmin 1, max 255 charsThe admin user’s full name
emailstringValid email formatThe admin user’s email address
passwordstringmin 8 chars, strength checkedThe admin user’s password
confirmPasswordstringMust match passwordClient-side only; not sent to API
acceptTermsbooleanMust be trueTerms of service acceptance

The registration form includes a client-side password strength meter that scores passwords on five criteria:

CriterionScore
At least 8 characters+1
Contains uppercase letter+1
Contains lowercase letter+1
Contains digit+1
Contains special character+1
ScoreLabel
0-1Too weak
2Weak
3Fair
4Good
5Strong

The server-side password check (isPasswordStrong) performs its own validation independently of the client-side indicator.


ENABLE_REGISTRATION / ENABLE_PARTNER_REGISTRATION

Section titled “ENABLE_REGISTRATION / ENABLE_PARTNER_REGISTRATION”

Partner self-registration is controlled by an environment variable that can be set on both the API and the frontend.

The API reads ENABLE_REGISTRATION from the environment using the envFlag utility. When set to false, both POST /auth/register and POST /auth/register-partner return:

{
"error": "Registration is currently disabled",
"code": "REGISTRATION_DISABLED"
}

Default: true (registration is enabled unless explicitly disabled).

Terminal window
# Disable registration
ENABLE_REGISTRATION=false
# Enable registration (default)
ENABLE_REGISTRATION=true

Accepted truthy values: 1, true, yes, on Accepted falsy values: 0, false, no, off


System-scoped users can create partners directly via the API. This bypasses self-registration and allows setting fields that self-registration does not expose (type, plan, device limits).

Terminal window
curl -X POST https://breeze.example.com/api/v1/orgs/partners \
-H "Authorization: Bearer <system-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme MSP",
"slug": "acme-msp",
"type": "msp",
"plan": "pro",
"maxOrganizations": 50,
"maxDevices": 5000,
"billingEmail": "[email protected]"
}'
FieldTypeDescription
iduuidAuto-generated primary key
namevarchar(255)Display name. Required
slugvarchar(100)URL-safe unique identifier. Required
typeenummsp, enterprise, or internal. Default: msp
planenumfree, pro, enterprise, or unlimited. Default: free
maxOrganizationsintegerOptional cap on organization count
maxDevicesintegerOptional cap on total devices across all organizations
settingsjsonbPartner-level settings (timezone, business hours, contact info)
ssoConfigjsonbSSO configuration
billingEmailvarchar(255)Billing contact email
createdAttimestampRecord creation time
updatedAttimestampLast modification time
deletedAttimestampSoft-delete marker. Null when active

Partner-scoped users can update their own partner’s name, billing email, and settings via the /partners/me endpoint. Settings are merged on update — only the keys included in the request are overwritten.

Terminal window
curl -X PATCH https://breeze.example.com/api/v1/orgs/partners/me \
-H "Authorization: Bearer <partner-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme IT Services",
"settings": {
"timezone": "America/Chicago",
"businessHours": { "preset": "business" },
"contact": {
"name": "Jane Doe",
"email": "[email protected]"
}
}
}'

Partners use soft deletes. When deleted via DELETE /partners/:id, the deletedAt timestamp is set. The partner and its data remain in the database but are excluded from all queries.


Users are linked to partners through the partner_users join table. Each record contains:

FieldTypeDescription
partnerIduuidFK to the partner
userIduuidFK to the user
roleIduuidFK to a role (determines permissions)
orgAccessenumall, selected, or none
orgIdsuuid[]Array of organization IDs when orgAccess is selected

When a user authenticates, the API resolves their token context by looking up the partner_users record. The JWT includes the partnerId, scope: partner, and the roleId. The orgAccess and orgIds fields are used at query time to filter which organizations the user can access.

Partner admins can invite users via the user management endpoints. Invited users receive an email with an invite link that leads to a password-setting form. Upon acceptance, the user is associated with the partner via partner_users with the specified role and organization access level.


MethodPathAuthDescription
POST/auth/register-partnerNoneSelf-service partner registration. Rate limited to 3/IP/hour

Request body:

{
"companyName": "Acme IT Services",
"email": "[email protected]",
"password": "securePassword123!",
"name": "Jane Doe",
"acceptTerms": true
}

Success response (201):

{
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "Jane Doe",
"mfaEnabled": false
},
"partner": {
"id": "uuid",
"name": "Acme IT Services",
"slug": "acme-it-services"
},
"tokens": {
"accessToken": "eyJ...",
"expiresInSeconds": 900
},
"mfaRequired": false
}

Error responses:

StatusCondition
400Validation error (weak password, missing required fields, terms not accepted)
429Rate limit exceeded (3 attempts per IP per hour)
503Redis unavailable (rate limiter cannot function)
MethodPathDescription
GET/orgs/partnersList all partners. Supports page and limit query params (default 50, max 100)
POST/orgs/partnersCreate a new partner
GET/orgs/partners/:idGet partner by ID. Returns 404 if not found or soft-deleted
PATCH/orgs/partners/:idUpdate partner fields. Returns 400 if no fields provided
DELETE/orgs/partners/:idSoft-delete partner. Sets deletedAt
MethodPathDescription
GET/orgs/partners/meGet the authenticated user’s partner details
PATCH/orgs/partners/meUpdate partner name, billing email, or settings (merged, not replaced)
MethodPathDescription
GET/orgs/organizationsList organizations accessible to the partner user. Supports pagination
POST/orgs/organizationsCreate a new organization under the user’s partner
GET/orgs/organizations/:idGet organization by ID (access-controlled)
PATCH/orgs/organizations/:idUpdate organization fields
DELETE/orgs/organizations/:idSoft-delete organization

Partner registration is rate-limited to 3 attempts per IP address per hour using the Redis-backed sliding window rate limiter. This is stricter than the general user registration limit (5 attempts per IP per hour). The rate limit key is derived from the client IP address extracted from the X-Forwarded-For header or the direct connection IP.

When a registration request uses an email that already exists, the API returns a generic success-like response rather than an explicit “email already exists” error:

{
"success": true,
"message": "If registration can proceed, you will receive next steps shortly."
}

This prevents attackers from using the registration endpoint to enumerate valid email addresses.

Partner slugs must be unique. The registration flow automatically appends a numeric suffix (-1, -2, etc.) if the generated slug already exists. This process attempts up to 100 suffixes before returning a 500 error. Slugs are capped at 50 characters from the base company name before the suffix.

If user creation fails after the partner and role records have been inserted, the API manually deletes the orphaned role and partner records. This is not wrapped in a database transaction, so in edge cases (e.g., the cleanup query itself fails), orphaned records may remain.


Registration form redirects to /login?reason=registration-disabled. The PUBLIC_ENABLE_REGISTRATION environment variable is set to false on the frontend. To enable registration, set PUBLIC_ENABLE_REGISTRATION=true in the frontend environment and restart the web server. Also verify that the API has ENABLE_REGISTRATION=true (or unset, since the default is true).

“Too many registration attempts. Try again later.” (429). The rate limiter has blocked the IP address after 3 registration attempts within one hour. Wait for the rate limit window to expire, or use a different IP address for testing. In production, verify Redis is running — if Redis is down, the endpoint returns 503 instead.

Registration succeeds but user cannot access any organizations. After self-registration, the partner has no organizations yet. The admin user is created with orgAccess: all, but there are no organizations to access. Create the first organization via POST /orgs/organizations or through the setup wizard.

“Unable to generate unique company identifier” (500). The slug generation exhausted 100 suffix attempts. This happens if many partners have been registered with very similar names. Provide a more distinctive company name, or create the partner manually via the system API with an explicit slug.

Partner not appearing in the partner list. Only system-scoped users can list all partners via GET /orgs/partners. Partner-scoped users cannot see other partners. Verify the user has scope: system in their JWT. Also check whether the partner was soft-deleted — soft-deleted partners are excluded from all list queries.

“Service temporarily unavailable” (503) during registration. Redis is not accessible. The rate limiter requires Redis to function. Check that Redis is running and the REDIS_URL environment variable is correctly configured on the API server.

Self-registered partner has free plan and msp type. This is the default behavior. Self-registration always creates partners with type: msp and plan: free. To change the plan or type, a system-scoped user must update the partner via PATCH /orgs/partners/:id.

Registration link not visible on the login page. The login page conditionally renders the registration link based on the PUBLIC_ENABLE_REGISTRATION feature flag. If the flag is false or unset as false, the link is hidden. Set PUBLIC_ENABLE_REGISTRATION=true to show it.