diff --git a/docs/license-faq.md b/docs/license-faq.md
index 1167b4a4..105f7f13 100644
--- a/docs/license-faq.md
+++ b/docs/license-faq.md
@@ -59,9 +59,9 @@ This split (AGPL core + commercial `ee/`) is the same model used by Cal.com and
---
-## Community and Business
+## Community, Starter, Team and Business
-A self-hosted instance runs **Community** unless a licence key is activated. Community includes up to 3 active users. **Business** adds more users, single sign-on (Entra ID, Google, Okta, OIDC) and SCIM provisioning; administrators can try it for 30 days under **Settings → License**. See [anythingmcp.com/pricing](https://anythingmcp.com/pricing).
+A self-hosted instance runs **Community** unless a licence key is activated. Community is meant for personal use, education and evaluation, and includes up to 3 active users. **Starter** and **Team** license an instance for small teams at work. **Business** adds more users, single sign-on (Entra ID, Google, Okta, OIDC) and SCIM provisioning; administrators can try it for 30 days under **Settings → License**. See [anythingmcp.com/pricing](https://anythingmcp.com/pricing).
---
diff --git a/packages/backend/src/ee/licensing/edition.service.spec.ts b/packages/backend/src/ee/licensing/edition.service.spec.ts
index 9aa4e13f..518af7e6 100644
--- a/packages/backend/src/ee/licensing/edition.service.spec.ts
+++ b/packages/backend/src/ee/licensing/edition.service.spec.ts
@@ -64,7 +64,7 @@ describe('EditionService', () => {
license: { plan: 'business', status: 'active', expiresAt: null, features: { maxUsers: 10 } },
});
const s = await service.getState();
- expect(s).toMatchObject({ edition: 'business', source: 'license', seatLimit: 10, trialAvailable: false });
+ expect(s).toMatchObject({ edition: 'business', business: true, source: 'license', seatLimit: 10, trialAvailable: false });
});
it('never gives a paid licence fewer users than Community', async () => {
@@ -74,6 +74,34 @@ describe('EditionService', () => {
expect((await service.getState()).seatLimit).toBe(COMMUNITY_SEAT_LIMIT);
});
+ it('licenses Starter and Team without SSO or SCIM, and still offers the Business trial', async () => {
+ for (const plan of ['starter', 'team']) {
+ const { service } = make({
+ license: { plan, status: 'active', expiresAt: null, features: { maxUsers: 3 } },
+ });
+ expect(await service.getState()).toMatchObject({
+ edition: plan,
+ business: false,
+ source: 'license',
+ seatLimit: COMMUNITY_SEAT_LIMIT,
+ trialAvailable: true,
+ });
+ }
+ });
+
+ it('lifts the user limit of a Starter key during the Business trial', async () => {
+ const { service } = make({
+ license: { plan: 'starter', status: 'active', expiresAt: null, features: { maxUsers: 1 } },
+ settings: { business_trial_ends_at: new Date(Date.now() + DAY).toISOString() },
+ });
+ expect(await service.getState()).toMatchObject({ edition: 'starter', business: true, source: 'trial', seatLimit: null });
+ });
+
+ it('refuses Business capabilities to a Starter key', async () => {
+ const { service } = make({ license: { plan: 'team', status: 'active', expiresAt: null } });
+ await expect(service.assertBusiness('Single sign-on')).rejects.toBeInstanceOf(ForbiddenException);
+ });
+
it('has no user limit when the licence carries none', async () => {
const { service } = make({
license: { plan: 'enterprise', status: 'active', expiresAt: null, features: { maxUsers: null } },
diff --git a/packages/backend/src/ee/licensing/edition.service.ts b/packages/backend/src/ee/licensing/edition.service.ts
index 28abaae2..f4f2d319 100644
--- a/packages/backend/src/ee/licensing/edition.service.ts
+++ b/packages/backend/src/ee/licensing/edition.service.ts
@@ -22,16 +22,18 @@ export const BUSINESS_TRIAL_DAYS = 30;
export const TRANSITION_DAYS = 60;
const PAID_PLANS = new Set(['starter', 'team', 'business', 'enterprise']);
+/** Paid plans that include single sign-on, SCIM and role sync. */
+const BUSINESS_PLANS = new Set(['business', 'enterprise']);
const TRIAL_KEY = 'business_trial_ends_at';
const TRANSITION_KEY = 'business_transition_until';
const DAY_MS = 24 * 60 * 60 * 1000;
-export type Edition = 'cloud' | 'community' | 'business';
+export type Edition = 'cloud' | 'community' | 'starter' | 'team' | 'business' | 'enterprise';
export type BusinessSource = 'license' | 'trial' | 'transition' | null;
export interface EditionState {
edition: Edition;
- /** Whether Business capabilities (SSO, SCIM, more users) are available. */
+ /** Whether Business capabilities (SSO, SCIM, role sync) are available. */
business: boolean;
source: BusinessSource;
/** Plan of the activated licence key, if any. */
@@ -53,9 +55,11 @@ export const SEAT_LIMIT = 'seat_limit';
/**
* Which edition a self-hosted instance runs, and what that allows.
*
- * Community is the default. Business comes from an activated licence key,
- * from the one-time evaluation, or from the transition period of an instance
- * that already relied on Business capabilities when it was upgraded.
+ * Community is the default. A Starter or Team key licenses the instance for
+ * its users. Business capabilities (SSO, SCIM, role sync) come from a
+ * Business or Enterprise key, from the one-time evaluation, or from the
+ * transition period of an instance that already relied on them when it was
+ * upgraded.
*
* The rules never take away something that already works: existing users stay
* active above the limit, existing single sign-on identities keep signing in,
@@ -136,32 +140,38 @@ export class EditionService implements OnModuleInit {
const trialActive = !!trialEnds && trialEnds > now;
const transitionActive = !!transitionUntil && transitionUntil > now;
- const source: BusinessSource = paid
+ const businessKey = paid && BUSINESS_PLANS.has(license!.plan);
+ const source: BusinessSource = businessKey
? 'license'
: trialActive
? 'trial'
: transitionActive
? 'transition'
- : null;
+ : paid
+ ? 'license'
+ : null;
+ const business = businessKey || trialActive || transitionActive;
+ // A paid key never allows fewer users than Community; the evaluation and
+ // the transition period have no limit, whichever key is active.
let seatLimit: number | null = COMMUNITY_SEAT_LIMIT;
- if (source === 'license') {
+ if (trialActive || transitionActive) {
+ seatLimit = null;
+ } else if (paid) {
const max = (license?.features as any)?.maxUsers;
seatLimit = typeof max === 'number' ? Math.max(COMMUNITY_SEAT_LIMIT, max) : null;
- } else if (source) {
- seatLimit = null;
}
return {
- edition: source ? 'business' : 'community',
- business: source !== null,
+ edition: paid ? (license!.plan as Edition) : business ? 'business' : 'community',
+ business,
source,
plan: license?.plan ?? null,
seatLimit,
seatsUsed,
communitySeatLimit: COMMUNITY_SEAT_LIMIT,
trialEndsAt: trialEnds?.toISOString() ?? null,
- trialAvailable: trialRaw === null && !paid,
+ trialAvailable: trialRaw === null && !businessKey,
trialDays: BUSINESS_TRIAL_DAYS,
transitionUntil: transitionActive ? transitionUntil!.toISOString() : null,
};
diff --git a/packages/frontend/src/app/settings/users/page.tsx b/packages/frontend/src/app/settings/users/page.tsx
index 84a4dc65..e3314ea5 100644
--- a/packages/frontend/src/app/settings/users/page.tsx
+++ b/packages/frontend/src/app/settings/users/page.tsx
@@ -278,7 +278,7 @@ export default function SettingsUsersPage() {
)}
- {seatsFull && !edition!.business && (
+ {seatsFull && edition!.edition === 'community' && (
{users} - {!edition.business && ` · Community includes up to ${edition.communitySeatLimit} users.`} + {edition.edition === 'community' && ` · Community includes up to ${edition.communitySeatLimit} users.`}
{error}
} - {edition.source !== 'license' && ( + {!(edition.source === 'license' && edition.business) && (