improvement(self-host): enterprise features enabling (#6028)

* improvement(self-host): enterprise features enabling

* chore(helm): bump chart to 1.3.0 for the enterprise self-host values

values.yaml gained the ENTERPRISE_ENABLED switch and INSTANCE_ORG_* keys, and
the feature-flag envDefaults moved from "false" to empty so the master switch
can resolve them. Additive and backward compatible, so a minor bump.

* fix(self-host): address review findings on instance org and org delete

Drop the per-process instance-org id cache. It went stale once the
organization was deleted through the Admin API, and clearing it from the
delete handler would only heal the replica that served that request. The
lookup runs on the signup path against a single-row table, so re-reading
costs nothing and keeps every replica self-correcting.

Scope the org-delete subscription conflict to entitled statuses. Matching any
row regardless of status let a canceled subscription — which bills nobody —
permanently block deletion.

* fix(admin): block org delete on any live subscription, not just entitled ones

ENTITLED_SUBSCRIPTION_STATUSES excludes trialing, so a trial — which grants no
entitlement but is a live Stripe subscription that will convert — slipped past
the delete guard and could be stranded against a removed organization id.

Adds TERMINAL_SUBSCRIPTION_STATUSES and inverts the predicate: block unless the
row is finished. Expressed as the terminal set so a status Stripe adds later
defaults to blocking, which is the safe direction for a destructive operation.

* fix(self-host): resolve SSO and access-control in the UI, not the raw env var

Nine client consumers still read NEXT_PUBLIC_SSO_ENABLED /
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED directly while the server gates and settings
nav had moved to the resolver. With only ENTERPRISE_ENABLED set that produced
dead ends: the SSO settings section appeared but ssoClient() was never
registered and no login button rendered, and the Access Control section
appeared but its page reported "not entitled".

Points every consumer at isSsoEnabled / isAccessControlEnabled so visibility and
capability come from one place.

* fix(admin): validate retention workspace targets on the Admin API too

retentionOverrides and per-workspace PII rules both name a workspace, and
neither field is a foreign key. The settings UI rejected ids belonging to
another organization; the Admin API did not, so the two paths could persist
different data for the same org.

Extracts the check as getForeignWorkspaceTargetsReason and points both routes
at it, so they cannot drift apart again.

* fix(self-host): close three review findings on admin routes and cleanup

Make org delete atomic. detachOrganizationWorkspaces committed on its own, so a
failed delete left workspaces detached and re-billed while the organization,
its members, and its settings survived. Adds a Tx variant so both commit
together.

Gate the admin session-policy PATCH on entitlement, matching the settings UI.
Without it the stored policy was inert — getSessionPolicy resolves to no-op when
the feature is off, so the one eager clamp would be undone on the next refresh.

Stop emitting plan-wide housekeeping when billing is off. It is keyed to the
hosted free-tier 30-day window, the same default the per-workspace pass
deliberately refuses to apply off-hosted.

* fix(admin): gate whitelabel on entitlement and emit detach audits post-commit

The Admin whitelabel PATCH skipped the entitlement check the settings UI runs,
so an admin key could set branding the product had not granted the org.

detachOrganizationWorkspacesTx also wrote its audit rows inside the caller's
transaction, contradicting its own doc comment — a rolled-back delete would have
left audit history describing detachments that never happened. It now returns
the rows and callers emit them after commit.

* fix(self-host): refuse instance-org resolution when the slug is ambiguous

organization.slug has no unique constraint, and the lookup took the first of
however many matched. The choice is unordered, so two replicas could resolve
different organizations and split new signups between them.

Resolution is now three-state. Ambiguity is distinct from absence, so it both
declines to adopt an arbitrary organization and declines to provision another
one on top of the duplicates.
This commit is contained in:
Vikhyath Mondreti
2026-07-28 19:02:41 -07:00
committed by GitHub
parent 591702b4aa
commit c809845b99
60 changed files with 3443 additions and 306 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
totalRoutes: 981,
zodRoutes: 981,
totalRoutes: 984,
zodRoutes: 984,
nonZodRoutes: 0,
} as const
+7
View File
@@ -385,6 +385,13 @@ export async function promptUnlocks(vars: Map<string, string>): Promise<Record<s
})
if (selected.length === 0) return {}
const flags = new Set(selected)
if (flags.has('ENTERPRISE_ENABLED')) {
p.log.info(
theme.muted(
'The enterprise switch covers every feature below — pick individual ones only to override it.'
)
)
}
if (flags.has('ACCESS_CONTROL_ENABLED') && !flags.has('ORGANIZATIONS_ENABLED')) {
flags.add('ORGANIZATIONS_ENABLED')
p.log.info(theme.muted('Access control requires organizations — enabling both.'))
+9 -1
View File
@@ -6,11 +6,13 @@
*/
export const FLAG_TWINS: ReadonlyArray<{ server: string; client: string }> = [
{ server: 'BILLING_ENABLED', client: 'NEXT_PUBLIC_BILLING_ENABLED' },
{ server: 'ENTERPRISE_ENABLED', client: 'NEXT_PUBLIC_ENTERPRISE_ENABLED' },
{ server: 'ACCESS_CONTROL_ENABLED', client: 'NEXT_PUBLIC_ACCESS_CONTROL_ENABLED' },
{ server: 'ORGANIZATIONS_ENABLED', client: 'NEXT_PUBLIC_ORGANIZATIONS_ENABLED' },
{ server: 'WHITELABELING_ENABLED', client: 'NEXT_PUBLIC_WHITELABELING_ENABLED' },
{ server: 'AUDIT_LOGS_ENABLED', client: 'NEXT_PUBLIC_AUDIT_LOGS_ENABLED' },
{ server: 'DATA_RETENTION_ENABLED', client: 'NEXT_PUBLIC_DATA_RETENTION_ENABLED' },
{ server: 'SESSION_POLICIES_ENABLED', client: 'NEXT_PUBLIC_SESSION_POLICIES_ENABLED' },
{ server: 'DATA_DRAINS_ENABLED', client: 'NEXT_PUBLIC_DATA_DRAINS_ENABLED' },
{ server: 'FORKING_ENABLED', client: 'NEXT_PUBLIC_FORKING_ENABLED' },
{ server: 'INBOX_ENABLED', client: 'NEXT_PUBLIC_INBOX_ENABLED' },
@@ -24,6 +26,11 @@ export const FLAG_TWINS: ReadonlyArray<{ server: string; client: string }> = [
/** Self-host feature unlocks offered by the wizard's Custom flow. */
export const SELF_HOST_UNLOCKS: ReadonlyArray<{ server: string; label: string; hint: string }> = [
{
server: 'ENTERPRISE_ENABLED',
label: 'All enterprise features',
hint: 'enables everything below',
},
{
server: 'ACCESS_CONTROL_ENABLED',
label: 'Access control',
@@ -31,7 +38,8 @@ export const SELF_HOST_UNLOCKS: ReadonlyArray<{ server: string; label: string; h
},
{ server: 'ORGANIZATIONS_ENABLED', label: 'Organizations', hint: 'multi-workspace orgs' },
{ server: 'AUDIT_LOGS_ENABLED', label: 'Audit logs', hint: '' },
{ server: 'DATA_RETENTION_ENABLED', label: 'Data retention', hint: 'retention policies' },
{ server: 'DATA_RETENTION_ENABLED', label: 'Data retention', hint: 'deletes expired data' },
{ server: 'SESSION_POLICIES_ENABLED', label: 'Session policies', hint: 'session lifetime caps' },
{ server: 'DATA_DRAINS_ENABLED', label: 'Data drains', hint: 'export streams' },
{ server: 'FORKING_ENABLED', label: 'Workflow forking', hint: '' },
{ server: 'INBOX_ENABLED', label: 'Inbox', hint: '' },