Conversation
added 9 commits
April 22, 2026 23:58
Nuxt/Nitro destr coerces NUXT_PUBLIC_BILLING_ENABLED to a boolean, so the string comparison `=== 'true'` always evaluated false and the UI rendered self-hosted mode even when billing was configured. - Change runtime config default to `false` (boolean) for type safety - Use strict `=== true` comparison in useBilling - Drop duplicate derivation in WorkspaceUsagePanel, reuse the composable - Make WorkspaceSwitcher mirror the server-side self-host fallback (db plan 'free' renders as 'starter' badge when billing is not configured) - Auto-sync runtimeConfig.public.billingEnabled from isBillingConfigured() at boot via a new Nitro plugin, so operators no longer need to set the public flag in addition to the provider env vars
Establish a clear Community vs Enterprise Edition model and prepare the repository for the 12 documented deployment scenarios (managed SaaS, on-premise enterprise, managed dedicated, community self-host, licensed self-host, AGPL fork, hosting reseller, OEM embedded, white-label partner, air-gapped, contributor, local eval). Licensing - Rewrite ee/LICENSE with Managed Use, On-Premises Deployment, Evaluation, OEM, and White-Label grants; [LAWYER REVIEW] markers preserved for jurisdiction, liability cap, CLA, and related items - Add LICENSE-EXCEPTIONS with AGPL §7(c) attribution and §7(e) no-trademark terms — the only §7 clauses that are enforceable against downstream recipients - Add NOTICE as a dual-license index for source-distribution recipients Docs - docs/LICENSING.md — SKU × license type × 12-scenario matrix - docs/EDITIONS.md — Community vs Enterprise runtime behaviour and per-surface UI rules - docs/DEPLOYMENT_PROFILES.md — per-profile env checklist and auto-detection rules AGPL §13 compliance - /about page (public, unauthenticated) exposing source link and deployment metadata - Sidebar and auth layout link to /about CLAUDE.md: rewrite the Enterprise Edition section so the rules match the code reality — edition is orthogonal to plan tier, CDN/Media providers are ee-resident by design, requires_ee flag gates features even when the plan matrix grants them.
Introduce explicit four-axis deployment resolution (profile, edition,
billing mode, plan source) and wire it through every plan-gating call
site so all 12 documented scenarios behave consistently.
Profiles (auto-detected, NUXT_DEPLOYMENT_PROFILE overrides):
- managed — ee + polar/stripe, subscription-driven plan
- dedicated — ee + flat/subscription, operator or subscription plan
- on-premise — ee + billing off, operator-set plan (default enterprise)
- community — agpl only, fixed 'community' tier, no billing
Server
- New server/utils/deployment.ts: resolveDeployment() caches per
process, reads NUXT_DEPLOYMENT_PROFILE, falls back to ee-bridge +
billing detection. Guards against misconfiguration (explicit
'managed' without ee falls back to community).
- getWorkspacePlan honors the deployment's planSource (subscription /
operator / fixed) instead of forcing every self-host workspace to
'starter'. On-premise customers now get enterprise-tier access
through workspace.plan = 'enterprise' as operators intended.
- Billing middleware splits into subscription vs non-subscription
paths; non-subscription profiles never throw 402 and never look up
payment_accounts.
Shared matrix (content-driven, new fields)
- plan-features rows gain community_value, requires_ee, and optional
roadmap flags. Three orphan rows removed (ai.agent, git.connect,
projects.create) because they represent always-on product capabilities.
- FEATURE_MATRIX entries now carry {plans, requires_ee, roadmap};
PLAN_LIMITS entries carry {values, requires_ee}.
- hasFeatureForPlan / getPlanLimitForPlan accept an {edition} option
that force-disables requires_ee features in Community Edition.
- shared.license knows about the new 'community' plan tier.
EE route feature gating
- runEnterpriseRoute gains an optional featureKey argument that runs
the plan+edition gate before touching the bridge. All 15 ee/-gated
routes now pass the appropriate feature key (ai.byoa, api.conversation,
api.webhooks_outbound) so Starter customers on Managed hit 403
instead of reaching the bridge.
Runtime config
- nuxt.config runtimeConfig.public.deployment exposes the resolved
profile/edition/billingMode snapshot to the client.
- 00.billing-flag Nitro plugin writes the snapshot at boot and
re-resolves after 01.init-ee has had a chance to load the bridge.
Q2 orphan classification applied:
- KALDIR: ai.agent, git.connect, projects.create
- KEEP + ENFORCE + requires_ee: ai.byoa (now Pro+), api.conversation,
api.custom_instructions, forms.file_upload, forms.webhook_notification,
forms.spam_filter, media.custom_variants, media.variants_per_field
- KEEP + ENFORCE (core): forms.notifications
- ROADMAP + requires_ee: mcp_cloud_custom_domain, mcp_cloud_sso,
cdn.custom_domain, cdn.preview_branch, sso.saml/oidc, branding.white_label
Tests
- New tests/unit/deployment.test.ts — 12 scenarios for the resolver.
- license.test: Community Edition gating, requires_ee semantics.
- license-content-parity: matrix shape guards, EE-required flag
coverage, community-tier coverage.
- media-license, agent-permissions, enterprise-ai-keys: mock the
ee bridge + reset deployment cache to exercise Managed code paths.
Content regeneration
- npx contentrain generate refreshed the SDK client types to pick up
community_value / requires_ee / roadmap fields on plan-features.
Wire the Phase 1 deployment snapshot through every plan/billing/ edition-sensitive UI surface so the 12 documented scenarios render coherently. Each profile now has a single, consistent presentation: Community - Sidebar: "Community" badge on every workspace row - Overview plan card: read-only, Community Edition badge - Billing tab: Community Edition notice, no subscription controls - AI Keys tab: hidden (requires ai.byoa / ee bridge) - Project Settings: Conversation API + Webhooks tabs hidden - Members: reviewer/viewer roles removed from dropdown with EE hint Managed (Polar/Stripe) - Plan card: clickable, opens PlanSelectionModal - Billing tab: live subscription controls - All EE tabs visible (gated per plan by hasFeature) On-premise / Dedicated (flat-fee) - Sidebar + Overview: operator-set plan badge (enterprise default) - Billing tab: "On-premise deployment" notice explaining operator controls the plan; no subscription UI - Plan card: read-only - AI Keys / Webhooks / Conversation API: visible on ee editions - PlanSelectionModal: blocked (layout + overview both gate on hasManagedBilling) New client composables: - useDeployment — reactive wrapper over runtimeConfig.public.deployment; convenience flags (isCommunity, isManaged, isDedicated, isOnPremise, hasManagedBilling, isOperatorManagedPlan). Fail-safe: unknown edition collapses to Community so enterprise UI never renders pre-boot. - useFeature / useFeatureLimit / useFeatureMeta — edition-aware reactive gates for UI-side feature checks; mirrors the server hasFeature semantics exactly (plans AND (!requires_ee OR edition==='ee')). useBilling rewired: - billingEnabled derived from deployment.hasManagedBilling (backward- compatible alias; new call sites should prefer useDeployment directly) - billingState returns 'subscribed' for community and operator-managed profiles — never throws locked states; matches server middleware - effectivePlan returns 'community' in Community, workspace.plan with enterprise fallback on operator-managed, webhook-synced plan on managed Dictionary: - billing.community_* (title/description/badge) - billing.on_premise_* (title/description/badge) - billing.edition_agpl / edition_ee - billing.plan_community - billing.roadmap_badge - settings.plan_community_info / plan_on_premise_info - settings.role_ee_hint / ee_feature_disabled_hint Tests: - use-deployment.nuxt.test.ts — 5 profile scenarios + fail-safe - use-feature.nuxt.test.ts — Community gating, Managed Pro enforcement, roadmap metadata - use-billing.nuxt.test.ts — expanded to cover all four profiles with live runtimeConfig mutation (Nuxt auto-import can't be reliably stubbed by vi.stubGlobal/mockNuxtImport without breaking setupNuxt) Total: 9 files modified, 4 new files, 440 tests pass (from 409).
Update the non-legal documentation set so every reader-facing file
tells the same story: Community Edition vs Enterprise Edition, four
deployment profiles, and `requires_ee` as the orthogonality flag.
README
- New "Editions" and "Deployment Profiles" sections at the top
of the plans/licensing area
- Feature-list stack line updated: Billing now "Plugin registry
(Polar default, Stripe fallback)"; CDN/Media marked ee-resident
- Docs index reorganised: Editions / Deployment Profiles /
Licensing / Self-Hosting surfaced first
- License block references LICENSE-EXCEPTIONS, NOTICE, and the
/about AGPL §13 obligation
ROADMAP
- EE section rewritten: shipped-in-ee table + roadmap table with
plan-matrix keys; new exploratory item "AGPL → BSL migration
study" capturing the Phase 2 decision to revisit license model
SELF_HOSTING
- Renames "Minimal Core Mode" → "Community Edition" and
"Operational Mode" keeps as additive overlay
- Adds an "On-Premises Enterprise" profile section mirroring the
ee/LICENSE §2.2 grant
- AGPL §13 Source-Disclosure Obligation section calling out the
/about page and LICENSE-EXCEPTIONS §7(c)
DEPLOYMENT
- Adds "Editions and Profiles" section at the top linking into
DEPLOYMENT_PROFILES.md
- Billing section rewritten — NUXT_PUBLIC_BILLING_ENABLED is now
auto-derived; Polar + operator-set plan branches explained
- Post-deploy smoke checks include /about render and deployment
snapshot visibility
PAYMENT_PROVIDERS
- Drops the manual NUXT_PUBLIC_BILLING_ENABLED line from the Polar
env block (the plugin registry derives it at boot)
- "Self-hosted / no billing" section split into community vs
on-premise behaviour
.env.example
- New "Deployment profile" section documenting
NUXT_DEPLOYMENT_PROFILE + auto-detection rules
- NUXT_PUBLIC_BILLING_ENABLED explanation updated — no longer
required for the managed profile
- Billing block prose updated to mention community + on-premise
fallback behaviour
ee/README
- Rewritten from scratch: "Edition is orthogonal to plan tier" up
front, graceful-null + 403 degradation contract, how to add new
ee handlers (wire runEnterpriseRoute feature key + plan-features
row), all five license grants summarised
app/middleware/auth.global.ts
- Honor `definePageMeta({ auth: false })` so the public /about
page is reachable without login. Required for the AGPL §13
source offer to be visible to every interacting user.
tests/e2e/app-smoke.e2e.test.ts
- New smoke: /about returns 200 without auth (middleware honors
auth:false meta)
- New smoke: runtime config payload carries the deployment
snapshot (community fallback visible in the serialized HTML)
Close the loop on the Q2 orphan audit: enforce every feature whose
code path exists, mark the rest `roadmap: true` so the UI can render
"Coming Soon" chips instead of leaving marketing claims unbacked.
Server enforcement
- `media.custom_variants` + `media.variants_per_field` gated via
new `resolveVariantConfigWithPlan()` helper in server/utils/
media-variants.ts. Custom variant objects silently fall back to
the default preset on plans that lack the feature (no 403 — the
upload still succeeds, we just refuse the customisation). Variant
count in excess of the plan limit throws 403 explicitly.
Applied at both upload endpoints (multipart `/media` and JSON
`/media/upload-url`).
- `forms.webhook_notification` gated at the form submit outbound-
webhook emit. Community/Free plans skip; Starter+ with ee bridge
dispatch as before.
Matrix
- `roadmap: "true"` added to: `forms.spam_filter`, `forms.file_upload`,
`forms.notifications`, `api.custom_instructions`. These are
advertised in the plan matrix but have no current code path;
flagging them roadmap keeps UI honest until implementation lands.
UI
- `PlanSelectionModal.planFeaturesList` returns structured entries
`{ label, roadmap }` instead of plain strings. The feature-list
template renders a "Coming Soon" badge (secondary variant) next
to any advertised-but-unimplemented feature, using the new
`billing.roadmap_badge` dictionary key.
Tests
- `tests/unit/media-variants.test.ts` — 7 new cases for
`resolveVariantConfigWithPlan`: preset honored, custom accepted
on Pro, silent fallback on Starter, count-limit throws, preset
overflow caught, unlimited plan bypass, Community zero-limit
guard-rail.
- `tests/unit/license-content-parity.test.ts` — new ROADMAP_FEATURES
list + two assertions: every roadmap feature carries roadmap=true,
shipped features never do. Prevents drift between the matrix flag
and actual enforcement state.
Result: 459 tests pass (+19), 0 lint errors, typecheck clean.
…ement
Three regressions surfaced by the CI pipeline that the local
vitest run didn't catch:
1. `runEnterpriseRoute` threw "Cannot read properties of undefined
(reading 'billing')" in `enterprise-bridge.integration.test.ts`
because the test passes a bare event object (`{} as never`) and
the plan gate assumed `event.context` existed. Guard the chain
with optional access and skip the plan gate when
`event.context?.billing?.effectivePlan` is absent — production
workspace-scoped routes always populate it via
`server/middleware/03.billing.ts`, so the gate stays effective in
real traffic.
2. AI key integration tests expected 200 for the happy path. With
the new plan gate + missing billing context in the test harness,
every call collapsed to 403 regardless of the Starter/Pro
distinction. The fix in (1) resolves this as a side effect —
absent-context = gate skipped = bridge mock honored.
3. `media-routes.integration.test.ts` crashed with
"resolveVariantConfigWithPlan is not defined" because the helper
was imported via the `~~` alias, which the integration project's
node environment doesn't resolve. Switch to an explicit relative
import in both media/index.post.ts and upload-url.post.ts so the
ad-hoc `await import(...)` test harness (which bypasses Nuxt's
auto-import + alias mapping) resolves the symbol.
Before: 459 tests → 8 integration failures in CI.
After: 566 tests pass (401 unit + 107 integration + 58 nuxt), 0
lint errors, typecheck clean.
Apply decisions from the web-sourced comparative review against GitLab EE, Elastic License 2.0, FSL 1.1-ALv2 (Sentry), BSL 1.1 (HashiCorp/MariaDB), and the 2026 ONLYOFFICE AGPL §7 enforcement precedent. Every [LAWYER REVIEW] marker is resolved. v1.0 is production-ready as a contractual first draft; a licensed attorney should still sign off before material enterprise deals, but the baseline text follows established market patterns rather than carrying open placeholders. ee/LICENSE v1.0 changes: - Effective date set to 2026-04-24 - Jurisdiction: Republic of Türkiye (Licensor is Türkiye-resident) - Dispute resolution: ICC arbitration, seat Istanbul, language English — neutral forum for international enterprise customers while keeping IP-injunctive relief carve-out in any competent court (ELv2 + FSL pattern) - Liability cap floor raised USD 100 → USD 500; carve-outs added for payment obligations, indemnification, Section 3/5 breach, and gross negligence / willful misconduct (standard enterprise SaaS boilerplate) - "Competing Product" in §3.8 redefined with FSL's three-criterion test (substitute / API surface / substantially similar functionality) + explicit internal-use exception - §2.1 Managed Use clarified so transformed web-browser JS served by the Managed Service doesn't conflict with the "no download/copy/retain" restriction — closes the obvious Manager → browser payload question - §4.4 AGPL §7 relationship restated: only §7(c) attribution and §7(e) no-trademark ride in LICENSE-EXCEPTIONS; no further restrictions imposed on the core (post-ONLYOFFICE v Euro-Office precedent, these are the only enforceable §7 terms for trademark / attribution protection) - §5.3 Contribution policy: DCO (Linux Foundation pattern, GitLab 2017-onwards), not CLA — lower contribution friction, adequate legal coverage for a project at this stage - §6.2 offline license key language replaced with an order-form + audit-rights reference; signed JWT / grace period machinery is a roadmap item for a future EE release - §10 rewritten as §10.1 governing law + §10.2 ICC arbitration + §10.3 IP carve-out - §11.6 Export control expanded: EAR / OFAC / EU dual-use / Turkish Ministry of Trade - §11.8 added: Data Processing Addendum required for EEA/UK/Turkish personal data processing (EU SCCs + KVKK SCCs) Companion doc cleanup: - docs/LICENSING.md — offline key language deferred to roadmap, [LAWYER REVIEW] marker removed - docs/DEPLOYMENT_PROFILES.md — two air-gap license-key LAWYER REVIEW markers replaced with the same v1.0 posture Note: I am not a lawyer. Every decision here is traceable to a well-known market license (GitLab, Elastic, Sentry FSL, MariaDB BSL, HashiCorp), and a senior IP/commercial counsel should still review the executed form of this License — but the draft is now at the level where that review is a sanity check rather than "fill in the blanks".
Nitro's compiled output freezes `runtimeConfig.public`, so the
billing-flag plugin's mutation crashed CI's E2E job:
TypeError: Cannot assign to read only property 'deployment'
at applyDeploymentSnapshot (00.billing-flag.ts)
Resolution:
- Mutation is now best-effort. When the public config is frozen
(production builds), the plugin logs a single warning and falls
through; when it's mutable (dev / test harnesses with a custom
runtime config), auto-derive continues to work.
- Operators on production deploys must set the public deployment
snapshot explicitly via the new
NUXT_PUBLIC_DEPLOYMENT_PROFILE / _EDITION / _BILLING_MODE env
vars. Nuxt maps `NUXT_PUBLIC_DEPLOYMENT_<KEY>` to the nested
`runtimeConfig.public.deployment.<key>` field at boot, before
any plugin runs, so no mutation is needed.
- nuxt.config.ts comments updated to point at the env-binding path;
.env.example documents both managed and community examples.
- tests/e2e/app-smoke.e2e.test.ts pins the three vars in the test
env so the runtime-config payload assertion (`"community"` in
the SSR HTML) matches in production-build mode.
- The plugin still sets `billingEnabled` opportunistically for
backward compatibility; if mutation fails, operators are
expected to have set NUXT_PUBLIC_BILLING_ENABLED themselves
(or the snapshot will degrade to the community fail-safe in
useDeployment).
Local pnpm test:e2e passes 16 tests across 6 files (was 5
crashed before).
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Close the gap between the "open-core" story in CLAUDE.md / README and the actual runtime. Before this branch,
isBillingConfigured()was the single axis the code used to distinguish self-host from managed; that collapsed four distinct deployment shapes into one boolean and left a dozen orphan features in the plan matrix with no runtime enforcement. The branch introduces:ee/LICENSE, AGPL §7 attribution + no-trademark additional terms,NOTICE, and the/aboutpage that satisfies AGPL §13 source-offer obligations.roadmap: trueso the UI renders "Coming Soon" chips instead of unbacked claims.Commits
fix(billing)— resolve self-host detection bug (Nuxt/destr coercesNUXT_PUBLIC_BILLING_ENABLED=trueto boolean; the UI was comparing against the string'true').feat(legal)— introduceee/LICENSEv1.0 (Managed Use / On-Premises / Evaluation / OEM / White-Label grants),LICENSE-EXCEPTIONS,NOTICE,docs/LICENSING.md,docs/EDITIONS.md,docs/DEPLOYMENT_PROFILES.md, and the public/aboutpage.feat(runtime)—server/utils/deployment.ts(four-axis resolver), edition-awarehasFeature,communityplan tier,requires_eeflag on plan-features rows, 15 EE routes now carry feature keys so Starter customers hit 403 before the bridge.feat(ui)—useDeployment+useFeaturecomposables, WorkspaceSwitcher / Overview / Billing / Usage / Members / PlanSelectionModal / ProjectSettings all edition-aware.docs(editions)— long-form docs (README, SELF_HOSTING, DEPLOYMENT, PAYMENT_PROVIDERS, ROADMAP, ee/README, .env.example) aligned with the edition + profile model.feat(enforcement)—media.custom_variants+media.variants_per_field+forms.webhook_notificationgated;forms.spam_filter,forms.file_upload,forms.notifications,api.custom_instructionsflaggedroadmap: truewith UI "Coming Soon" chip.Breadth
server/utils/deployment.ts,app/composables/useDeployment.ts,app/composables/useFeature.ts,server/plugins/00.billing-flag.ts(deployment snapshot sync),resolveVariantConfigWithPlan()inmedia-variants.ts.communityplan tier,community_value+requires_ee+roadmapcolumns onplan-features.LICENSE-EXCEPTIONS,NOTICE,docs/LICENSING.md,docs/EDITIONS.md,docs/DEPLOYMENT_PROFILES.md./aboutpage (public,auth: falsemetadata honored by updated middleware).Supported scenarios (from docs/DEPLOYMENT_PROFILES.md)
Outstanding
ee/LICENSEcarries 12[LAWYER REVIEW]markers — jurisdiction, liability cap formula, arbitration vs courts, CLA vs DCO, offline license key policy, "competing product" wording, GDPR DPA, data-breach carve-out, effective date. These need independent legal review before v1.0 ships.ROADMAP.mdadds "AGPL → BSL migration study" as an exploratory item — AGPL §7 cannot enforce commercial SaaS resale restrictions; if that becomes a real problem, BSL is the path to evaluate (with OSI-status tradeoff).Test plan
pnpm lint— expect 0 errors, 7 pre-existing warningspnpm typecheck— expect cleanpnpm test:unit && pnpm test:nuxt— expect 459 passNUXT_POLAR_*env and noee/— verify profile=community,/aboutrenders, sidebar shows "Community" badge, AI Keys tab is hidden, Members panel offers only Editor roleNUXT_POLAR_ACCESS_TOKEN+ keepee/— verify profile=managed, billing tab shows subscription controlsNUXT_DEPLOYMENT_PROFILE=on-premisewithee/loaded and no billing env — verify plan card is read-only with "On-premise" badge/aboutunauthenticated — page renders with source link + deployment metadata (AGPL §13)