Files
triggerdotdev--trigger.dev/.server-changes
Matt Aitken 269470fd87 feat(webapp): gate SSO on an entitlement instead of the Enterprise plan (#4393)
The SSO & Directory Sync settings page decided access by comparing the
organization's plan code against the literal string `"enterprise"`. The
webapp now reads a `hasSso` entitlement from plan limits.

## Changes

- **`settings.sso` route** — `planAllowsSso` reads `limits.hasSso`
rather than the plan code; the loader and the action gate on a shared
`getSsoEntitlement` helper.
- **`platform.v3.server`** — new `getSsoEntitlement(orgId)` returning
`entitled | not_entitled | unknown`, behind a new SWR cache namespace
(60s fresh / 120s stale, memory + Redis). This replaces an uncached
billing round-trip that previously ran on every settings load, so the
page gets cheaper than it was.
- **`directorySyncEffects`** — the entitlement is now checked before
applying membership effects, per organization and memoised across a
batch.
- **`@trigger.dev/platform` 1.2.0 → 1.3.0** — required, see below.

## Behaviour worth reviewing

**Revocation now stops SCIM.** Previously the plan check existed only on
the settings page, so an org that lost access kept receiving
directory-sync pushes indefinitely; only the config UI froze. Provision
*and* deprovision are gated, so a revoked entitlement can't remove
members either.

**An unreadable entitlement throws instead of skipping.** Effects are
idempotent and the worker retries, so retrying is lossless where
dropping would silently lose a directory change. It's raised at `warn`
level so a transient billing blip doesn't page anyone.

**The login path is deliberately untouched.** A hard entitlement check
there turns a billing outage into a login outage. Consequence: an org
that loses the entitlement keeps its existing SSO logins working until
the connection is removed. Gating sign-in is a separate decision.

**Self-hosted is unaffected.** With no billing service configured the
helper returns `entitled`, leaving plugin presence and the kill switch
as the only gates — a self-hoster who installed the plugin isn't locked
out of it.

## The dependency bump is load-bearing

The `Limits` schema is a plain `z.object`, so it *strips* unknown keys.
On 1.2.0 the `hasSso` field was silently discarded during parsing and
read as `undefined` no matter what billing sent — a structural accessor
would not have helped. Verified against both builds:

```
1.2.0 → parsed: true | hasSso survives: false
1.3.0 → parsed: true | hasSso survives: true
```

This PR therefore cannot merge before 1.3.0 is published, which it now
is.

## Testing

`apps/webapp/test/directorySyncEffects.server.test.ts` — 7 tests over
the gate: applies when entitled, skips provision and deprovision when
not, throws a warn-level retryable error when unreadable, resolves once
per org across a batch, and gates per org so one unentitled org doesn't
block another.

`pnpm run typecheck --filter webapp` passes (18/18), oxfmt and oxlint
clean.
2026-07-27 13:08:16 +01:00
..

Server Changes

This directory tracks changes to server-only components (webapp, supervisor, etc.) that are not captured by changesets. Changesets only track published npm packages — server changes would otherwise go undocumented.

When to add a file

Server-only PRs: If your PR only changes apps/webapp/, apps/supervisor/, or other server components (and does NOT change anything in packages/), add a .server-changes/ file.

Mixed PRs (both packages and server): Just add a changeset as usual. No .server-changes/ file needed — the changeset covers it.

Package-only PRs: Just add a changeset as usual.

File format

Create a markdown file with a descriptive name:

.server-changes/fix-batch-queue-stalls.md

With this format:

---
area: webapp
type: fix
---

Speed up batch queue processing by removing stalls and fixing retry race

Fields

  • area (required): webapp | supervisor
  • type (required): feature | fix | improvement | breaking

Description

The body text (below the frontmatter) is a one-line description of the change. Keep it concise — it will appear in release notes.

Writing guidance

These entries are public-facing - they ship verbatim in user-visible release notes. A few rules to keep them clean:

  • Write for the user, not the reviewer. Lead with what the user notices or has to do. If a reader who doesn't know the codebase can't tell what changed for them, rewrite it.
  • One sentence is usually enough. The body is the bullet in the changelog. If you need a paragraph, you're probably describing the implementation rather than the change.
  • Describe behavior, not implementation. Skip internal scopes, middleware names, library specifics, framework internals. Users care about what's different for them, not how it's wired.
  • Never name internal tools or infra. Observability stacks, internal services, infra components, monitoring backends, CI surfaces, AWS specifics - none of these belong in user-facing notes.

Before / after:

  • "The image verification step now parses the manifest's layer media types and returns a new result the finalizer rejects." (describes the wiring; a user can't act on it)
  • "Deploying with an outdated CLI could produce an image that fails to start on every run. These deploys are now stopped before going live, with a message asking you to upgrade the CLI and re-deploy." (what the user sees and does)

Lifecycle

  1. Engineer adds a .server-changes/ file in their PR
  2. Files accumulate on main as PRs merge
  3. The changeset release PR includes these in its summary
  4. After the release merges, CI cleans up the consumed files

Examples

New feature:

---
area: webapp
type: feature
---

TRQL query language and the Query page

Bug fix:

---
area: webapp
type: fix
---

Fix schedule limit counting for orgs with custom limits

Improvement:

---
area: webapp
type: improvement
---

Use the replica for API auth queries to reduce primary load