Skip to content

v6.0.0

Latest

Choose a tag to compare

@Tobbe Tobbe released this 25 Aug 09:09
· 2998 commits to main since this release

Highlights

CedarJS v6 focuses on three things

  1. A new Fragment Cell for cutting query waterfalls between components
  2. A build pipeline that's fully Vite instead of half Babel
  3. Deploying to a plain container host with little to no configuration.

On top of that it also adds enforced job timeouts and cancellation, new configureGraphQLServer/configureServer API hooks, and a batch of fixes aimed at AI coding agents.

This is a big release with a number of breaking changes.

Read the full highlights and upgrade guide: https://cedarjs.com/docs/canary/upgrade-guides/cedar-v6

The complete PR-by-PR changelog for all 340 PRs/commits in this release is below.

Changelog

🚀 Features

feat(cli): Auto-redact sensitive fields from sdl generation (#2368) by @Tobbe

Fixes #2285 (problem 2 — problem 1 was fixed in #2365).

cedar generate sdl User on a dbAuth project used to expose hashedPassword, salt, resetToken, etc. directly in the generated GraphQL SDL, making them queryable (and settable) by anyone the @requireAuth directive lets through.

The generators now exclude dbAuth's auth fields (hashedPassword, salt, resetToken, resetTokenExpiresAt, webAuthnChallenge) by name from everything that touches the GraphQL API:

  • SDL: the type and the Create/Update inputs (inputs too, so no one can overwrite a password hash via updateUser). Relation stubs from #2365 get this automatically since they share the same code path.
  • Scaffold: cells, forms and display pages, so scaffolded components never query fields that no longer exist in the SDL.
  • Service tests: test inputs match the redacted input types; the create test is skipped when a redacted field is required in the database (a create through the generated input can't succeed for auth models). Scenarios keep all fields — they create rows via Prisma directly.

Since salt is a generic word that can be benign on its own (Recipe.salt), it's only treated as sensitive when the model has at least one other auth field. After generating, the CLI prints a note naming any excluded fields and how to add them back manually.

feat(cli): Generate stubs for relations (#2365) by @Tobbe

Part of #2285 (solves problem 1 — the generator not being relation-aware; problem 2, sensitive-field leaking, will be addressed separately).

Problem

Running cedar generate sdl Message, where Message has a relation to User with no SDL of its own, wrote the SDL/service files and then failed type generation with:

Unknown type: "User"

This left the project in a broken state, with no indication that the fix is to generate SDL for the related model. Worse, because Prisma relations are bidirectional there is no command order that avoids the error on a fresh project: generate sdl User fails symmetrically with Unknown type: "Message". Today the "fix" only works because the first command's broken files are already on disk when you run the second one.

Solution

generate sdl now detects related models that don't have SDL files yet and generates read-only stubs for them in the same run, so type generation runs once, at the end, and succeeds:

✔ Generating SDL files...
✔ Generating types ...

Message has relations to models that don't have SDL files of their own yet: User
Read-only SDL stubs were generated for them, since GraphQL type generation fails otherwise.
To replace a stub with a full SDL and service, run
  yarn cedar generate sdl User

A stub is the GraphQL type plus a list query — no mutations, no test files — and starts with a header explaining why it exists:

// Generated as a read-only stub by `cedar generate sdl Message`,
// because Message has a relation to User, which had no SDL yet.
// Run `cedar generate sdl User` to replace this stub with the real thing.
// If you edit this file, the hash below will stop matching and you'll
// need to pass `--force` to overwrite it.
// @cedar-generator-stub-hash d94c8f09bc1c00b4

export const schema = gql`
  type User {
    ...
  }

  type Query {
    users: [User!]! @requireAuth
  }
`
How it works
  • Detection (missingRelatedModels in the new stubFiles.ts): walks the model's Prisma relations transitively, handling circular relations (Message ↔ User). A related type counts as "defined" if any existing *.sdl.{ts,js} file defines it (content check, so renamed or hand-written SDL files are respected).
  • Replacing stubs: the @cedar-generator-stub-hash marker holds a hash of the stub's generated content. When a later generate sdl User (or generate scaffold User) targets a stub that hasn't been edited, it's overwritten without --force. If the stub has been edited, the generator refuses with a clear message so user changes are never silently lost. The hash only covers content below the marker line.
  • The overwrite logic lives in a new writeFilesWithStubsTask alongside the sdl generator — the shared writeFile/writeFilesTask in lib/ are untouched.
Scope notes
  • The scaffold generator doesn't generate stubs yet (follow-up PR — its tests need broader updates), but its writes are stub-aware, so scaffolding a model whose stub exists works without --force.
  • destroy sdl only ever destroys the named model's own files, never stub paths.
  • Fixed a latent bug where inputSDL mutated the module-level DEFAULT_IGNORE_FIELDS_FOR_INPUT array — harmless before, but real once a single run generates multiple models.
  • Updated the Troubleshooting Generators docs (the old "generate everything and ignore the errors" workflow now only applies to scaffolds) and the generate sdl section in the CLI reference.

Testing

  • New sdlStubs.test.ts covering detection (cycles, transitive relations, already-defined types), stub content, hash pristine/edited detection, and all overwrite paths through the real Listr task.
  • Handler-level snapshot coverage of stub files in both JS and TS modes.
  • CI=1 yarn vitest run src/commands/generate src/commands/destroy in packages/cli — 967 tests pass.
  • yarn eslint clean on changed files; tsc --noEmit error count identical to main (all pre-existing).
feat(cli): Extract the SQLite->PostgreSQL switch into `setup database postgres` (#2331) by @Tobbe

Closes #2303. Supersedes item #6 of docs/implementation-plans/2026-08-03-deploy-simplification.md, per that issue.

Summary

setup neon did two genuinely different jobs in one command: converting a project's schema/dependencies/adapter from SQLite to PostgreSQL (nothing to do with Neon specifically), and provisioning + configuring a Neon database. That made the conversion unavailable to anyone using a different Postgres provider (Railway, Render, Fly, Coolify, DigitalOcean, ...), even though it's the single biggest source of friction in any non-SQLite deploy.

This extracts the provider-agnostic half into yarn cedar setup database postgres:

  • Removes SQLite dependencies and dependenciesMeta
  • Switches schema.prisma to the postgresql provider
  • Rewrites api/src/lib/db.ts to the PrismaPg adapter — this was never Neon-specific in content, it's plain @prisma/adapter-pg, just living under setup/neon/templates/
  • Updates prisma.config to read DIRECT_DATABASE_URL for migrations
  • Adds @prisma/adapter-pg
  • Runs prisma migrate dev if DATABASE_URL is already in .env, with a clear note if it isn't yet (there's nothing to provision here, so nothing to wait on before migrating)

setup neon now calls into the same task list (getSqliteToPostgresTasks) and adds only what's actually Neon-specific on top: provisioning via the Neon API, and writing DATABASE_URL/DIRECT_DATABASE_URL to .env.

Behavior change from the extraction

The schema/adapter/config steps used to also skip whenever DATABASE_URL was already present in .env (guarding setup neon against re-provisioning on top of an existing database). That guard is still correct for provisioning, but it didn't belong on the schema/adapter/config steps — their own idempotency checks (already on postgresql? already using PrismaPg?) are what should gate them. Gating them on DATABASE_URL too meant a project with DATABASE_URL set by hand but never actually converted would get its schema flipped to postgresql while db.ts and prisma.config were left untouched — an inconsistent half-converted state, and also the wrong default for the new standalone command's primary use case (setting DATABASE_URL from a provider dashboard before running conversion). Provisioning/env-writing/migrating in setup neon still skip on an existing DATABASE_URL, now via a --force-gated skipProvisioning flag local to that handler.

feat(web): Add CedarProvider. Deprecate RedwoodProvider (#2376) by @Tobbe

Continuing the rebranding to Cedar.

This should be a backwards-compatible change. The old provider name is still exported, it's just deprecated

feat(api): Nudge users to use `request` instead of `event` (#2168) by @Tobbe

event was the older AWS Lambda Request shaped event. request is the new web standards Request object.

event, and now request, is what you get on the third parameter to getCurrentUser(), and also in your authDecoder, if you have a custom one

⠶ request present (fetch-native paths):
  ┌───────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐
  │ Call site                                             │ Context                                              │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ runtime.ts → buildCedarContext →                      │ Fastify + vite dev middleware passes native Request. │
  │ getAuthenticationContext(request)                     │ requestToBaseEvent normalizes event, request gets    │
  │                                                       │ the raw Request                                      │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ useRedwoodAuthContext.ts →                            │ GraphQL Yoga — getAuthEvent() returns                │
  │ getAuthenticationContext(context.request)             │ context.request (web Request)                        │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ initDbAuthMiddleware → getCurrentUser(..., { event:   │ MiddlewareRequest (superset of Request)              │
  │ req, request: req })                                  │                                                      │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ initSupabaseAuthMiddleware → getCurrentUser(..., {    │ MiddlewareRequest                                    │
  │ event: req, request: req })                           │                                                      │
  └───────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘

⠶ request absent (Lambda-only paths):

  ┌───────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐
  │ Call site                                             │ Context                                              │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ graphql.ts → getAuthenticationContext(event, context) │ Lambda APIGatewayProxyEvent — no Request to pass.    │
  │                                                       │ isFetchApiRequest is false, event stays raw Lambda   │
  │                                                       │ event, request is undefined                          │
  ├───────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
  │ useRequireAuth.ts → getAuthenticationContext(event,   │ Lambda event from legacy requireAuth wrapper — same  │
  │ context)                                              │ as above                                             │
  └───────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘

So request is populated on all modern fetch paths (buildCedarContext, GraphQL Yoga via web Request, middleware) but absent on the two true Lambda paths (the legacy graphql.ts handler and requireAuth).

feat(graphql-server,api): add Cedar-named variants, deprecate Redwood-named public APIs (#2317) by @lisa-assistant

Summary

Follow-up to #2315. That PR renamed internal-only Redwood-named identifiers. This one covers the remaining public API identifiers still named after Redwood — since these are genuinely public, this PR adds Cedar-named variants as the canonical implementation and deprecates (but does not remove) the Redwood-named originals via @deprecated JSDoc + alias exports. This is a non-breaking change.

Yoga plugins (@cedarjs/graphql-server)
  • useRedwoodDirectiveuseCedarDirective
  • useRedwoodAuthContextuseCedarAuthContext
  • useRedwoodErroruseCedarError
  • useRedwoodGlobalContextSetteruseCedarGlobalContextSetter
  • useRedwoodLoggeruseCedarLogger
  • useRedwoodPopulateContextuseCedarPopulateContext
  • useRedwoodOpenTelemetryuseCedarOpenTelemetry
  • useRedwoodTrustedDocumentsuseCedarTrustedDocuments
Supporting public types
  • RedwoodDirectiveCedarDirective
  • useRedwoodDirectiveReturnUseCedarDirectiveReturn (also capitalized to follow the PascalCase convention used by other type names)
  • RedwoodTrustedDocumentOptionsCedarTrustedDocumentOptions
  • RedwoodOpenTelemetryConfigCedarOpenTelemetryConfig
  • RedwoodScalarConfigCedarScalarConfig
@cedarjs/api
  • RedwoodErrorCedarError
  • RedwoodLoggerOptions (@cedarjs/api/logger) → CedarLoggerOptions

Each deprecated export carries a @deprecated Use `CedarX` instead JSDoc comment pointing at its replacement. All internal usages throughout graphql-server and api now use the new Cedar names directly.

feat(jobs): enforce maxRuntime and support cancelling jobs (#2436) by @Tobbe

Fixes #2414

Implements the two things asked for in the issue — an actually-enforced per-job timeout and a way to cancel queued/running jobs — plus a fix for the stale-lock check that the issue's analysis surfaced.

1. maxRuntime is now enforced

The Worker passes maxRuntime down to the Executor, which races perform() against a timer. When a job exceeds maxRuntime:

  • The timeout is recorded in the job's lastError (as a JobTimeoutError) and the job is marked permanently failed via adapter.failure() — it is not retried and no other worker will silently re-execute it. Not retrying is deliberate: the timed-out attempt's promise can't be killed and may still be holding resources, so an automatic re-run could mean two copies of the job doing (expensive) work at once.
  • An AbortSignal is aborted so the job can stop its own work. Jobs access it via the new getJobExecutionContext() export (backed by AsyncLocalStorage, same pattern as @cedarjs/context):
import { getJobExecutionContext } from '@cedarjs/jobs'

perform: async () => {
  const context = getJobExecutionContext()
  await fetch(url, { signal: context?.signal })
  // or wire it to child processes, or check signal.aborted in loops
}

Node promises aren't truly cancelable, so the worker "stops waiting + marks failed + aborts the signal" rather than killing the promise. A child-process executor (truly killable) is still a possible future step; this removes the silent-double-execution and no-visibility problems today without that architectural change.

2. Jobs can be cancelled

  • BaseAdapter gets an optional cancel({ jobId }) method (optional so existing third-party adapters keep compiling; calling cancel through an adapter without it throws CancelNotImplementedError).
  • PrismaAdapter.cancel() marks the job permanently failed (failedAt set, lastError: 'Job cancelled by user') via updateMany guarded on failedAt: null. Returns true/false for whether a cancellable job was found. No schema change needed in user apps.
  • Queued job → never runs, record kept for visibility.
  • Running job → the in-flight attempt isn't interrupted (the new timeout bounds it), but it won't be retried and won't be picked up by another worker; the failedAt marker is sticky across the attempt's own error() update.
  • PrismaAdapter.schedule() (and therefore later()) now returns the created job record so app code has an id to cancel/poll. The scheduler function returned by createScheduler() gets a cancel attached:
const scheduledJob = await later(SampleJob, [args])
await later.cancel(scheduledJob.id)

3. Bug fix: stale-lock cutoff in PrismaAdapter.find()

The cutoff was computed as now + (maxRuntime || DEFAULT_MAX_RUNTIME * 1000) — a future date (with the * 1000 only applied to the default), so lockedAt < cutoff was true for every locked job and any in-progress job was immediately eligible for pickup by another worker. It's now now - (maxRuntime || DEFAULT_MAX_RUNTIME) * 1000, i.e. a lock only goes stale after maxRuntime seconds, matching the documented behavior. (Inherited from upstream Redwood's original jobs commit.)

Notes on API changes

  • later() / Scheduler.schedule() previously resolved to true; it now resolves to the adapter's schedule() return value (the created job record for the PrismaAdapter, which is still truthy). Custom adapters whose schedule() returns void will see undefined instead of true.
  • Timed-out jobs are now failed instead of being left locked and later re-run — that's the behavior change the issue asks for.

Testing

  • yarn workspace @cedarjs/jobs test (vitest incl. type tests): 133 passed
  • New unit tests: Executor timeout/abort-signal/timer-cleanup, PrismaAdapter cancel() + schedule() return + stale-lock cutoff, Scheduler cancel() + pass-through return, JobManager later.cancel wiring, plus type-level tests for the scheduler's new return type and cancel
  • npx tsc --noEmit, eslint, prettier --check on the package: clean

Docs updated in docs/docs/background-jobs.md: new "Cancelling Jobs" and "Job Timeouts" sections, updated maxRuntime config description, and adapter-authoring notes for cancel()/schedule().

feat(create-cedar-app): Add `build` and `start` scripts to app templates (#2294) by @Tobbe

Implements #2 from docs/implementation-plans/2026-08-03-deploy-simplification.md.

Problem

Generated Cedar apps have no scripts key in their root package.json at all.

Zero-config builders — Railpack (Railway), Nixpacks, Paketo (DigitalOcean), Google Cloud buildpacks, Heroku — detect a start command in the order start script → mainindex.js, and run build if one is defined. A Cedar app offers none of those, so detection finds nothing and every container host forces you into config-as-code just to boot the app.

Change

Adds to the root package.json of all four templates and all six package-manager overlays:

"scripts": {
  "build": "cedar build",
  "dev": "cedar dev",
  "start": "cedar serve",
  "start:api": "cedar serve api",
  "start:web": "cedar serve web"
}

The cedar bin comes from @cedarjs/core (packages/core/package.json:13), already a root devDependency in every template, so it resolves from node_modules/.bin under yarn, npm and pnpm alike.

Overlays are included because they replace the base template's root package.json wholesale (see the comment in scripts/generateLockfile.js) — without them npm and pnpm projects would miss out. The two CI-verified fixtures (test-project, test-project-esm) are updated to match.

Topology decision this encodes

start is the single-container path and start:api / start:web are the two-service path. Both ship deliberately:

  • Single-container is the convenient topology. It needs no service-to-service wiring — the web server proxies to the api in-process — which is precisely why one start script plus PORT handling yields a working deploy with no per-platform integration to write or maintain.
  • api-process + static/CDN web remains the recommended topology. It's what the generated Dockerfile, the baremetal nginx setup and the Render blueprint all use, and it avoids serving assets through the un-tuned @fastify/static.

Considered and rejected: start scripts in api/package.json and web/package.json. Railway's JS monorepo autodetection would pick them up and stage a service per package (it keeps the monorepo root and uses workspace-filtered commands, so installing works). But it's Railway-only, and it still leaves the proxy target unset — a half-staged two-service deploy that fails until you find the right setting is worse than one service that works immediately.

Platform support is two-tier, not uniform

start resolves the cedar bin from a root devDependency. That's fine on platforms that keep devDependencies around for the start step. It is not fine on the ones that prune them by default before running start:

  • Genuinely zero-config — Railway (Railpack), Render, Google Cloud Run, Coolify, Dokku, Dokploy, Koyeb, Northflank. Nothing extra needed.
  • Supported, but not zero-configHeroku and DigitalOcean App Platform (Paketo) both strip devDependencies after build by default, so start fails there (command not found) unless pruning is disabled — NPM_CONFIG_PRODUCTION=false / YARN_PRODUCTION=false on Heroku, YARN2_SKIP_PRUNING=true / NPM_CONFIG_PRODUCTION=false on DigitalOcean. With that one env var set, both work fine.

#2302 tracks moving start off the CLI and onto a runtime dependency (@cedarjs/api-server's cedarjs-server bin), which would make Heroku and DigitalOcean genuinely zero-config too. Not blocking this PR on it — landing now with the caveat above documented, since the CLI-based start already unblocks the platforms in the first tier and is a strict improvement over today's no-scripts-at-all baseline everywhere else.

Pairs with #2292

That PR makes PORT apply to the public side only, so under cedar serve the web side picks up $PORT while the api stays on 8911 internally.

feat(structure): Warn when unprotected routes use @requireAuth mutations (#2380) by @Tobbe

Fixes #2291

Summary

cedar check (via @cedarjs/structure's printDiagnostics) now emits a Warning for routes that are not wrapped in <PrivateSet>/<Private> but whose page — transitively, via its import graph — uses a GraphQL mutation whose root field carries a literal @requireAuth directive in the api-side SDL.

How it works:

  1. SDL directive map (RWProject.requireAuthMutationFields, in packages/structure/src/model/RWProject.ts): a lazy getter that walks RWProject.sdlsRWSDL.implementableFields and builds Map<mutationFieldName, sdlFilePathRelativeToProjectRoot> for every Mutation-type root field carrying a literal @requireAuth directive. Each SDL file is parsed in isolation (try/catch per file) so one malformed SDL doesn't hide valid @requireAuth fields declared elsewhere.
  2. Transitive import walk (packages/structure/src/model/util/pageMutationUsage.ts, new module): given a page's file path, does a BFS over ImportDeclaration module specifiers (resolving relative imports and the src/web/src/ alias, trying .tsx/.ts/.jsx/.js and /index.*), skipping anything that doesn't resolve inside web/src. In each reachable file it looks for gql/graphql tagged template expressions, concatenates the static (quasi) parts of the template, parses it with graphql's parse, and collects the top-level selection field names of any mutation operation definitions. Results are cached per file at the project level (via a WeakMap<RWProject, …>) since many pages share components. A visited set guards against import cycles.
  3. The diagnostic (RWRoute.*diagnostics()): when a route isn't private and its page resolves, intersects the page's transitively-used mutation fields with the project's @requireAuth map and yields a Warning per hit, anchored at the route's JSX node, e.g.:

Route 'adminPosts' is not wrapped in , but its page uses the mutation 'deletePost', which is marked @requireAuth in api/src/graphql/posts.sdl.ts (found in web/src/components/Post/Post.tsx)

A new RWError.UNPROTECTED_ROUTE_USES_AUTH_GATED_MUTATION code is attached.

Scoping decisions

  • Mutations only, not queries. Auth-gated queries rendered conditionally on a public page (e.g. showing extra data once logged in) are a common, legitimate pattern; only mutations (which cause side effects) are flagged.
  • Literal @requireAuth only@skipAuth and any custom directives are ignored. Keeps the check simple and avoids false positives from project-specific auth directives with different semantics.
  • DiagnosticSeverity.Warning, never Error — this is a best-effort heuristic (see limitations below), so it must never fail cedar check or block CI; it's a nudge, not a hard rule.

Known limitations

  • Interpolated gql/graphql templates are handled best-effort: the static (quasi) parts of the template are concatenated and the interpolated parts are dropped, which is usually fine for extracting the operation's field shape but can occasionally fail to parse — such documents are silently skipped.
  • Shared components (e.g. a <DeleteButton> used from both a protected and an unprotected route) can cause the warning to fire even when the actual call site guarding is fine at the page level, purely because the mutation is reachable from an unprotected page's import graph. This is a deliberate trade-off — hence Warning, not Error.
  • The import walk does not follow re-exports (export * from './x') or resolve TypeScript path aliases beyond the src/web/src/ convention.

Testing

  • yarn workspace @cedarjs/structure build — pass
  • CI=1 yarn workspace @cedarjs/structure test — 38/38 tests pass (7 test files)
  • New dedicated fixture project at packages/structure/src/model/__tests__/__fixtures__/mutation-auth-check/ (not one of the shared repo-root __fixtures__/ projects, since those are snapshotted by other packages), with new tests in packages/structure/src/model/__tests__/mutationAuthCheck.test.ts covering:
  1. Unprotected route whose page directly uses a @requireAuth mutation → warning, with route/mutation/SDL/component names in the message
  2. Same page, but route wrapped in <PrivateSet><Set>…</Set></PrivateSet> (nested-Set case from #2379) → no warning
  3. Mutation reached transitively (page → component → component with the gql tag) → warning
  4. Mutation marked @skipAuth → no warning
  5. @requireAuth query (not mutation) on an unprotected page → no warning
  6. Import cycle between two components → terminates without crashing, no warning
  • npx prettier --check / npx eslint on all changed/added files — pass
  • Ran the full structure suite against the existing example-todo-main / example-todo-main-with-errors fixtures used by model.test.ts: no snapshot changes. Verified directly that RWProject.requireAuthMutationFields is empty for both (example-todo-main's mutations are all @skipAuth; example-todo-main-with-errors's only @requireAuth-adjacent field is declared under type Query, not type Mutation, and one of its SDL files has an intentionally-malformed schema string, which is now isolated per-file rather than aborting the whole map).

Stacking

This PR is stacked on #2379 (tobbe-fix-structure-isprivate-nested-sets), which fixed RWRoute.isPrivate to walk nested <Set>/<PrivateSet> JSX ancestors — this PR's nested-Set warning-suppression test (case 2 above) depends on that fix. Base is set to tobbe-fix-structure-isprivate-nested-sets; please retarget to main after #2379 merges.

⚠️ feat(serve)!: Deploy related env vars `PORT`, `CEDAR_*`, etc (#2292) by @Tobbe

Two related deploy fixes that together let a Cedar app deploy to a container host with no configuration files at all.

Breaking change: serve api --ud now goes through the shared helpers instead of its own inline process.env.PORT ?? '8911'. That means its default host goes from localhost to :: in dev / 0.0.0.0 in production, matching every other serve path.

1. Read PORT and HOST

Every container PaaS (Railway, Render, Fly.io, Cloud Run, Heroku, App Runner) tells an app what to bind to via PORT/HOST. Cedar only read its own env vars, so all of them needed an explicit --port in the start command. Only the --ud api path honored PORT, which made it inconsistent as well as inconvenient.

PORT can't just be read by both sides. cedar serve runs the api and web servers in a single process, so both would bind it and collide with EADDRINUSE on exactly the platforms this is meant to help. It's therefore opt-in per side via isPublicSide:

  • web is public whenever both sides are served together (it proxies api requests)
  • api is public only when served on its own

Applied at the four call sites that know the topology, with a regression test pinning it.

Also adds a NaN guard. A non-integer port now throws a clear error instead of silently binding a random port.

2. Rename port/host env vars to CEDAR_*

CEDAR_API_PORT, CEDAR_WEB_PORT, CEDAR_API_HOST and CEDAR_WEB_HOST are now the primary names. The REDWOOD_* ones still work as silent fallbacks, matching how other CEDAR_*/REDWOOD_* vars already handle their aliases.

The lookup lives in a new readEnvVar in @cedarjs/project-config, which every affected package already depends on. That also removes the host/port duplication that existed between @cedarjs/api-server and @cedarjs/web-server.

Resolution order

  1. CLI flags (--port, --host, --api-port, …)
  2. CEDAR_API_PORT / CEDAR_WEB_PORT (and _HOST equivalents)
  3. PORT / HOST — public side only
  4. [api].port / [web].port in cedar.toml
  5. Built-in default
feat(cli): Add `cedar dev --node-args`, pass `--no-maglev` on Windows CI (#2106) by @Tobbe

Summary

The Windows smoke tests are the single biggest source of CI flakiness. A survey of the last 100 merged PRs (in docs/implementation-plans/flaky-smoke-tests-investigation.md) found that V8's Maglev JIT crash on Windows (nodejs/node#62260STATUS_STACK_BUFFER_OVERRUN / exit code 3221226505) accounts for ~52% of all flaky failures: it takes down the cedar dev web server mid-run, and every subsequent request fails with net::ERR_CONNECTION_REFUSED.

--no-maglev eliminates the crash, but it's a V8 flag — it can't be set via NODE_OPTIONS and can't be forwarded through the package-manager bin shim, so it has to be an actual node CLI arg on the process running the web dev server.

What this does

  • New cedar dev --node-args="..." flag — forwards arbitrary CLI args to the node process running the web/unified dev server (e.g. --inspect, --max-old-space-size=8192, --no-maglev).
  • Explicit launch — the dev-server bin is launched via node / yarn node "<binPath>" (path resolved from @cedarjs/vite/package.json) instead of the bin shim, so node flags can be applied. yarn node is used under Yarn so it also works with the PnP linker; bare node under npm/pnpm (which always have a real node_modules tree). Resolution failure throws loudly rather than silently degrading — @cedarjs/vite is a direct CLI dependency, so if it can't be resolved the install is broken.
  • No cross-envNODE_ENV is set via the concurrently job env (as the api/unified jobs already did). Dropping cross-env removes a whole node process from the chain, so the explicit launch is actually leaner than the old shim command.
  • CI passes --no-maglev the way a user would — via --node-args from the Windows dev-type smoke Playwright configs. --no-maglev is deliberately not hardcoded in the framework, so CI dogfoods the real mechanism and the code path stays exercised even after Node fixes the bug and we drop the flag.

Testing

  • yarn eslint + yarn prettier --check clean on all changed files.
  • tsc reports no new errors in the changed files (two pre-existing dev.ts TS2578 diagnostics are unrelated).
  • All 15 dev.test.ts unit tests pass, covering the yarn, npm and pnpm launch shapes plus --node-args forwarding. The unit-test job also runs on windows-latest.

Not yet validated / follow-ups

  • Verified structurally only — not yet run against a real Windows CI runner or a Yarn-PnP project (this PR's CI is the first real Windows exercise).
  • Not covered yet: cedar serve, streaming cedar-dev-fe (so streaming-ssr-dev is unchanged), the api-server watch bins, and storybook.
  • Signature D (Ubuntu esbuild "service is no longer running") is a separate, non-Maglev issue.
feat(web): Add CedarApolloProvider. Deprecate RedwoodApolloProvider (#2118) by @Tobbe

Continuing the Redwood -> Cedar rebranding effort I now also deprecated <RedwoodApolloProvider>

The new <CedarApolloProvider> has also moved to its separate file and to also move away from the bad pattern of barrel exports the new component is only available as a direct @cedarjs/web/apollo/CedarApolloProvider import.

feat(data-migrate): Use vite to run migrations (#2098) by @Tobbe

In an effort to consolidate on Vite usage everywhere, I'm moving data-migrate from bundle-require (which uses esbuild under the hood) to vite. And with that we can also use all our vite plugins

⚠️ feat(deps)!: Upgrade to MSW 2 (#2133) by @Tobbe

Upgrades MSW from 1.3.5 to 2.15.0.

Full upstream migration guide: https://mswjs.io/docs/migrations/1.x-to-2.x


TL;DR for Cedar App developers

Most apps need no changes. Cedar's mocking API (mockGraphQLQuery, mockGraphQLMutation, mockCurrentUser, and *.mock.ts cell mocks) is preserved as-is, including the ctx helper and the 'once' / 'networkError' response enhancers. You also don't need to bump anything yourself — MSW reaches your app transitively through @cedarjs/testing.

You only need to act if your own test files import from msw directly, or if you've customised the Jest web preset.

Who this affects

MSW is used by web-side Jest tests and Storybook. It is not wired into the api side, so nothing there is affected.

ESM/Vitest apps need no migration either, though for a subtler reason worth stating precisely: nothing on that path ever calls startMSW, and the mocking features you use there work through channels this upgrade doesn't touch. Generated cell tests pass mock data in as props (render(<Success {...standard()} />)) and never issue a GraphQL request at all, and mockCurrentUser() is read directly by the mocked useAuth rather than through an intercepted query. The flip side is that ESM apps can't currently rely on MSW interception at all — tracked separately in #2134, pre-existing and not caused by this PR.

What keeps working — no action needed

All of this is unchanged:

// Cell mocks — the most common case by far
export const standard = () => ({ blogPost: { id: 42, title: 'Mocked title' } })

// Explicit mocks in tests and stories
mockGraphQLQuery('GetArticle', { article: { id: 1 } })
mockGraphQLQuery('GetArticle', (variables, { ctx, req }) => {
  ctx.delay(500)
  ctx.errors([{ message: 'Uh oh' }])
  return { article: { id: variables.id } }
})
mockGraphQLQuery('GetArticle', () => data, 'once')
mockCurrentUser({ name: 'Rob', roles: ['admin'] })

MSW v2 removed the (req, res, ctx) resolver signature that ctx came from, so Cedar now reimplements that shape internally on top of v2's HttpResponse. It's a Cedar compatibility layer rather than an MSW API from this release onward, and we expect to deprecate it in a later release in favour of returning an HttpResponse directly — but nothing breaks now, and there'll be a separate migration when that happens.

Storybook's web/public/mockServiceWorker.js is regenerated automatically on the next cedar storybook run and is gitignored, so there's nothing to commit.

What needs action

1. Test files that import from msw directly. This is the real breaking change. If you registered your own handlers rather than going through Cedar's helpers, port them to the v2 API:

// Before (MSW 1)
import { graphql } from 'msw'
import { setupServer } from 'msw/node'

setupServer(
  graphql.query('GetUser', (req, res, ctx) => res(ctx.data({ user: { id: 1 } }))),
)

// After (MSW 2)
import { graphql, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

setupServer(
  graphql.query('GetUser', () => HttpResponse.json({ data: { user: { id: 1 } } })),
)

Note also that setupWorker moved from msw to msw/browser.

2. import 'whatwg-fetch' in your tests. MSW v2 uses the platform Fetch API, and the web Jest environment now provides Node's native fetch, Request, Response, AbortSignal, streams, and structuredClone. The polyfill is no longer needed and @cedarjs/testing no longer depends on it — if you have that import in your own test or setup files, remove it (or add whatwg-fetch to your own devDependencies if you still want it).

3. Customised Jest config. The default web/jest.config.js is just preset: '@cedarjs/testing/config/jest/web' and needs no changes. But the preset now sets two things you'd clobber by overriding them wholesale:

  • testEnvironment — now jest-fixed-jsdom (via Cedar's subclass), which restores the Node globals jest-environment-jsdom strips, and opts out of jsdom's browser-style export resolution so msw/node resolves at all.
  • transform / transformIgnorePatterns — MSW's CJS build depends on ESM-only packages (rettime, until-async, @open-draft/deferred-promise). Node supports require(esm); Jest's runtime doesn't, so the preset compiles those to CommonJS. If you set your own transformIgnorePatterns, keep the node_modules ESM exemption or MSW will fail to load.

4. Old projects with a committed mockServiceWorker.js. Newer Cedar projects gitignore this file, but if yours predates that and has a v1 worker checked in, delete it — a stale worker version mismatches the v2 client and MSW will warn. It regenerates on the next cedar storybook run.

5. TypeScript. MSW v2 requires TypeScript >= 4.8.

Framework-internal changes (no app impact)

  • mockRequests.ts rewritten on the v2 API; the v1 ctx transformers are reimplemented as a compat layer.
  • Web Jest environment now extends jest-fixed-jsdom instead of hand-copying Node globals. This also fixes a latent bug: jsdom's AbortSignal is a different class from Node's, so passing one to Node's Request threw Expected signal to be an instance of AbortSignal — MSW's request cancellation paths hit this.
  • packages/web/src/bins/msw.ts fixed — v2 moved its entry point, so require.resolve('msw') now lands in lib/core/ and the old relative ../package.json lookup broke the msw init bin used by Storybook setup.
  • packages/auth tests ported to v2 handlers; whatwg-fetch dropped.

Testing

  • Framework suites: @cedarjs/testing (8), @cedarjs/auth (14), @cedarjs/web (89), storybook-framework-cedarjs (17) — all pass.
  • local-testing-project (CJS/Jest) with packed tarballs: 17 suites / 45 tests pass, and cedar storybook --smoke-test exits 0 while generating a v2 mockServiceWorker.js through the fixed bin wrapper.
feat(api-server): add configureGraphQLServer and configureServer options (#2389) by @lisa-assistant

Fixes #2304, where registering @fastify/compress via configureApiServer only compressed api-function responses, not GraphQL responses.

configureApiServer and the GraphQL endpoint are registered as sibling Fastify plugins, each with their own encapsulation context, so a hook registered inside one never applied to the other. Two new options make this explicit instead of surprising:

  • configureGraphQLServer — the GraphQL-only counterpart to configureApiServer, scoped to just the GraphQL routes.

  • configureServer — runs on the root Fastify instance before the api functions and GraphQL plugins register their routes. This is the place for plugins with a "global" mode that hooks Fastify's onRoute (e.g. @fastify/compress), since those only affect routes registered after the plugin itself:

    const server = await createServer({
      configureServer: (server) => {
        server.register(compress, { global: true })
      },
    })

Plain request-lifecycle hooks (onRequest, onSend, etc.) don't depend on registration order and can still be added directly to the server instance returned by createServer() after the fact, applying to both api functions and GraphQL.

feat(pm): Explicit deps and Vite pin for ESM projects on pnpm and npm (#2178) by @Tobbe

Summary

Makes ESM projects work under pnpm and npm by declaring dependencies that were previously reachable only through yarn hoisting, and by giving the pnpm/npm overlays the vitest devDependency and Vite 7 pin the yarn overlay already has.

What pnpm's strict isolation surfaced

Config, setup, and test files import packages the workspace packages never declared:

  • web/vite.config.ts imports vite
  • api/vitest.config.mts imports vitest/config and @cedarjs/vite/api
  • web/vitest.setup.ts imports @testing-library/jest-dom and @testing-library/react
  • test files import @cedarjs/testing/web
  • api code imports graphql-tag, and the auto-imports transform injects import { context } from '@cedarjs/context'

Under yarn (and npm's flat hoisting) these resolve from the root node_modules; under pnpm each workspace package only sees what it declares. All of the above are now explicit in the ESM templates (esm-ts and esm-js), which is equally correct under yarn — one shared template for all package managers. The ESM fixture is regenerated to match (the diff is exactly these additions plus a caret-range refresh the codemods picked up).

Overlay changes

  • npm overlay: vitest devDependency and "vite": "7.3.5" in overrides
  • pnpm overlay: vitest devDependency and vite: '7.3.5' in pnpm-workspace.yaml overrides
  • This matches the yarn overlay, which already ships vitest plus the pin via resolutions — all three package managers are now consistent. The pin is what stops Vitest 4 from resolving its own nested Vite 8 (verified under pnpm: exactly one vite@7.3.5 in the store, vitest@4.1.10 resolves against it)
  • pnpm overlays (cjs and esm) also set autoInstallPeers: false, matching what CI's package-manager conversion already required: auto-installed peers resolve from the registry, bypassing overrides, which let published packages shadow tarball-synced framework packages during framework testing

Testing

With this plus #2177: fresh pnpm and npm ESM test projects pass cedar build --no-prerender, cedar test web (17 files / 45 tests) and cedar test api (10 files / 23 tests) end to end. A fresh yarn ESM project passes the same suites, confirming no yarn regression.

feat(cli): generate stubs for missing relations in scaffold (#2417) by @Tobbe

Summary

  • Extends the read-only SDL/service stub generation added for the SDL generator (#2365) to generate scaffold, so scaffolding a model with a relation to a model that has no SDL yet no longer breaks GraphQL type generation.
  • files() itself stays stub-free — destroy scaffold reuses it and must not delete stub files that other models may still depend on. The stub merging happens only in the write step of tasks(), mirroring how destroy sdl also uses the stub-free sdlHandler.files().
  • Once the scaffold finishes, the CLI prints two ways to replace a stub: generate sdl (SDL + service only) and generate scaffold (adds pages, cells, and forms too) — since scaffolding the related model is a reasonable next step, but not always the desired one. generate sdl's own equivalent message is left as-is (single option), since a user working with SDL alone is likely to want to continue that way.
  • Docs (schema-relations.md, cli-commands.md) updated to match; the old "scaffold doesn't support this yet" caveat is removed.

Test plan

  • Added scaffoldStubs.test.ts: verifies stub generation for a model with a missing relation, confirms stubs are read-only/untested, confirms files() alone stays stub-free (protecting the destroy scaffold contract), and confirms models without relations produce no stubs.
  • Added scaffoldStubRoundTrip.test.ts: scaffolds a model that stubs out a related model, then scaffolds that related model for real, and confirms the stub SDL/service are replaced with the full CRUD versions (without --force) and the web-side files are created.
  • yarn vitest run src/commands/generate/scaffold src/commands/generate/sdl src/commands/destroy — 667 tests pass.
  • yarn eslint / yarn prettier --check on all touched files — clean.
  • yarn build (via pre-push hook) — succeeds.
feat(cca): Package manager specific lock files (#2190) by @Tobbe

Don't put any lock file in the base template. Generate each of them into their respective overlay directory. This makes sure a project doesn't end up with two lockfiles (the (yarn) one from the base + whatever pm they selected (npm or pnpm))

⚠️ feat(cli)!: Remove `yarn cedar console` (#2137) by @Tobbe

Move the CLI console to a separate package

Run yarn dlx @cedarjs/console to start the console.

It now lives in https://github.com/cedarjs/console. See usage examples etc there

⚠️ feat(babel)!: Guard babel config behind reactCompiler flag (#2150) by @lisa-assistant

Fixes #2080.

Changes:

  • Only include babel-plugin-react-compiler when the React Compiler is active (no longer pulls in the entire default Babel config)
  • Exposes a babel option on cedar() so users can pass custom Babel config if needed:
    cedar({ babel: { plugins: ['my-babel-plugin'] } })
  • User-provided babel config is merged with the compiler plugin (if active), so both can coexist
feat(cli): add --migrations and --verbose flags to setup neon (#1830) by @lisa-assistant

cedar setup neon now supports --migrations/--no-migrations to control whether Prisma migrations run after provisioning (prompting when omitted in an interactive terminal, and erroring instead of hanging when omitted in a non-interactive one), and --verbose/-v to stream the full migration output instead of only showing it on failure.

⚠️ feat(eslint)!: Remove support for legacy ESLint config format (#2244) by @Tobbe

Summary

Follow-up to #2243. Flat config (eslint.config.mjs) has been the default for all new apps since v2.1.0 (#629, 2025-12-04), with a deprecation warning for legacy config added the very next day (#651). That warning has been shown by default ever since with no automated migration path -- #2243 fixed a bug in the legacy path itself (eslint-plugin-import vs -x), which raised the question of whether that path should even still exist. This PR removes legacy ESLint config support entirely, along with everything that existed only to support it.

This is a breaking change: projects still using .eslintrc.js or package.json's eslintConfig field with @cedarjs/eslint-config must migrate to flat config before upgrading past this version. There is no codemod for this (see packages/eslint-config/README.md for manual migration steps) -- projects have had the deprecation warning since v2.1.0 to act on.

Migrating from Legacy (.eslintrc.js) Config

The legacy .eslintrc.js/package.json#eslintConfig format is no longer supported. Follow these steps to migrate to flat config:

  1. Create a new flat config file in your project root:

    // eslint.config.mjs (for CommonJS projects)
    // or eslint.config.js (for ESM projects with "type": "module")
    import cedarConfig from '@cedarjs/eslint-config'
    
    export default await cedarConfig()
  2. Remove old config:

  • Delete .eslintrc.js if it exists
  • Remove eslintConfig field from package.json if it exists
  1. Update your package.json scripts (if needed):

    {
      "scripts": {
        "lint": "eslint .",
        "lint:fix": "eslint . --fix"
      }
    }
  2. Migrate custom rules: If you had custom rules in your old config, add them to your new flat config:

    export default [
      ...(await cedarConfig()),
      {
        rules: {
          // Your custom rules here
        },
      },
    ]

That's it! Your linting should work the same as before.

⚠️ feat(web)!: Remove GraphQL client-agnostic indirection (#2110) by @Tobbe

Cedar inherited code from Redwood that was meant to let apps swap Apollo for another GraphQL client (the GraphQLHooksProvider context and a set of overridable global types). The feature was never fully implemented and no one uses it, so it has been removed. Cells and the hooks exported from @cedarjs/web now call Apollo directly.

What this means for existing apps:

  • Unaffected: import { useQuery, useMutation, useSubscription } from '@cedarjs/web' (they are now Apollo's hooks re-exported), Cells (including fragment Cells and useFragment), mockGraphQLQuery/mockGraphQLMutation in tests and Storybook, and the graphQLClientConfig prop on RedwoodApolloProvider.
  • Breaking: GraphQLHooksProvider is no longer exported from @cedarjs/web, and Cells and the @cedarjs/web hooks now require an Apollo client in React context. Apps that used GraphQLHooksProvider to plug in a non-Apollo GraphQL client must switch to Apollo — either RedwoodApolloProvider or their own ApolloProvider setup. There is no longer any way to power Cells with a different GraphQL client; using one would mean bypassing Cells and the @cedarjs/web hooks entirely in favor of that client's own APIs.
  • Breaking: the ambient global types QueryOperationResult, MutationOperationResult, GraphQLQueryHookOptions, GraphQLMutationHookOptions and GraphQLOperationVariables no longer exist. Import the equivalent types (QueryResult, MutationTuple, QueryHookOptions, MutationHookOptions, OperationVariables) from @apollo/client instead.
feat(vite): Add Vite job path injector plugin to buildApp (#2099) by @Tobbe

Use the dedicated vite-plugin-cedarjs-job-path-injector instead of relying on the Babel version for job path injection. This is part of the ongoing effort to convert Babel-only transforms to pure Vite plugins.

The Vite plugin uses ast-grep for AST matching and is more performant than the Babel equivalent.

⚠️ feat(deps)!: Upgrade to Vitest 4 (#2131) by @Tobbe

Upgrades Vitest from 3.2.6 to 4.1.10 across the framework and the ESM app templates.

See the Vitest 3 -> 4 migration guide here https://vitest.dev/guide/migration.html

Who is affected

Only ESM Cedar apps run their tests with Vitest, so only they are affected. CJS Cedar apps keep testing with Jest and need no changes.

What Cedar app developers need to pay special attention to

Dependencies
  • Bump vitest to 4.1.10 in your app's root package.json.
  • Make sure your root package.json has a "vite": "7.3.5" entry in resolutions. New apps created from the ESM template already have it. Without it, Vitest 4 installs its own nested Vite 8, which breaks the JSX transform in web-side tests (Parse failure: Unexpected JSX expression).
Mocking behavior changes (most likely to break app tests)
  • Mocks called with new need constructible implementations. vi.fn(() => obj) used to work for code doing new MockedThing(); in Vitest 4 the implementation is invoked as a constructor, so arrow functions throw ... is not a constructor. Use a function expression or a class instead: vi.fn(function () { return obj }).
  • vi.spyOn on an already-spied method returns the existing spy including its accumulated call history, instead of a fresh one. Call counts can leak between tests — clear spies in beforeEach/afterEach (spy.mockClear() or vi.restoreAllMocks()).
  • vi.restoreAllMocks() (and the restoreMocks config option) now only restores vi.spyOn spies. It no longer touches vi.fn() mocks from vi.mock factories — use vi.resetAllMocks() or vi.clearAllMocks() for those.
Snapshots
  • Obsolete snapshots now fail the run in CI mode (Obsolete snapshots found when no snapshot update is expected). Remove stale entries locally with yarn vitest run <file> -u and review the diff.
Test hooks
  • beforeAll/afterAll callbacks now receive (context, suite) — the suite moved to the second argument, and the first parameter must use object destructuring (e.g. ({ task }) in test callbacks). If a test relied on beforeAll(async (ctx) => ctx.file.filepath), it now needs async ({}, suite) => suite.file.filepath.
If you customized your Vitest config
  • poolOptions was removed. singleThread/singleFork become maxWorkers: 1, isolate: false; for plain serial file execution use fileParallelism: false (now also supported in project-level configs). Cedar's API-side test preset handles serializing api test files against the shared test database automatically — no app action needed.
  • minWorkers, --minWorkers, maxThreads/minThreads, and the workspace option are gone (workspace -> projects).
  • test.env values set to undefined are now stringified to "undefined" instead of being removed — delete such keys instead.
  • The default exclude no longer contains **/dist/**. Cedar app builds don't emit test files into dist, but if you have compiled test artifacts lying around inside a test root, add your own exclude.
Misc
  • require('vitest') from CommonJS modules now throws — Vitest can only be imported.
  • Custom Vitest environments should use viteEnvironment instead of the deprecated transformMode (already handled inside @cedarjs/testingscenario() and describeScenario() keep working unchanged).
feat(cells): Add fragment Cells for query aggregation (#2107) by @Tobbe

Cells can now declare their data requirements with a FRAGMENT export instead of firing a query of their own. A parent Cell spreads the fragment in its QUERY and passes the matching slice of the query result down as a prop named after the fragment, so nested Cells no longer create request waterfalls – one single GraphQL request fetches everything.

// AuthorCell.tsx
export const FRAGMENT = gql`
  fragment AuthorCell_author on User {
    id
    email
    fullName
  }
`

export const Success = ({ author }) => <span>{author.fullName}</span>
// BlogPostCell.tsx
export const QUERY = gql`
  query FindBlogPostQuery($id: Int!) {
    post(id: $id) {
      id
      title
      author {
        ...AuthorCell_author
      }
    }
  }
`

export const Success = ({ post }) => (
  <article>
    <h2>{post.title}</h2>
    <AuthorCell author={post.author} />
  </article>
)

Fragment Cells automatically register their fragment with the GraphQL client, so spreading it by name is enough – no imports or interpolation needed. When the fragment selects the type's id, the Cell reads its data live from the Apollo cache and re-renders when mutations update the entity. See the new "Fragment Cells: Aggregating Queries" section in the Cells docs.

feat(vite): run background jobs in-process under Unified Dev (--ud) (#2444) by @lisa-assistant

Follow-up to #2442. Under Unified Dev (--ud), background job workers now run in-process through the same Vite server that serves api/src, instead of requiring a separate cedar-jobs work process against a manually-built api/dist.

feat(codegen): Read Prisma model names from the generated models file (#2464) by @ladderschool

GraphQL codegen imported the whole generated Prisma client just to read Prisma.ModelName — the only thing it takes from the client. On one production app that was 4.5 MB of generated TypeScript plus the @prisma/client runtime, to learn 38 strings. Because the import used a cache-busting query string, Node would load a fresh copy of that client on every codegen run, leaking memory over time.

For a TypeScript client, codegen now reads the model names straight out of the models.<ext> barrel file Prisma writes next to it, instead of importing the client at all. That's faster, uses less memory, and stops the leak. Legacy JavaScript clients keep the existing dynamic import.

feat(upgrade-scripts): Error out on bad eslint config (#2471) by @Tobbe

The v6 upgrade script now detects a leftover legacy ESLint config or a missing @cedarjs/eslint-config dependency and tells you what to do about it.

feat(upgrade-scripts): Fail the upgrade on a broken ESLint setup (#2476) by @Tobbe

The legacy-config and missing-dependency checks added for ESLint now abort the upgrade instead of just warning, since yarn cedar lint won't run afterward otherwise. Every other pre-upgrade check still runs first, so a project with several problems is told about all of them in one pass rather than discovering the next one on a retry.

feat(upgrade-scripts): Warn about RedwoodProvider in 6.x.ts (#2479) by @Tobbe

The v6 upgrade script now warns if your app still uses the deprecated RedwoodProvider — rename it to CedarProvider. This check already existed in the canary upgrade script; it was just missing from the v6 one.

feat(ci): Nightly cleanup of orphaned staging dist-tags (#2488) by @Tobbe

Internal: a nightly job now sweeps up leftover staging-* npm dist-tags from prerelease publishes that got cancelled mid-flight, so they don't accumulate indefinitely on published packages.

🛠️ Fixes

Click to see all 91 fixes
  • fix(cli): `exec` script args handling and types (#2433) by @lisa-assistant
  • fix(cli): track tsconfig.tsbuildinfo for rollback in package generator (#2390) by @lisa-assistant
  • fix(ci): don't force full CI on docs-only pushes for prettier-only baseline failures (#2454) by @lisa-assistant
  • fix(upgrade-scripts): Bring canary.ts up to date (#2453) by @Tobbe
  • fix(ci): pass --migrations to setup neon in E2E workflows (#2452) by @lisa-assistant
  • fix(cca): use CedarLoggerOptions in template comments (#2374) by @lisa-assistant
  • fix(cli): Improve neon setup DATABASE_URL handling (#2372) by @Tobbe
  • fix(cli): replace literal placeholder in prisma command output (#2370) by @Tobbe
  • fix(cli): Fix json typo in uploadsHandler (#2371) by @Tobbe
  • fix(cli): make sdl generate stub hint package-manager agnostic (#2369) by @Tobbe
  • fix(api-server): Read PORT when the api side is served standalone (#2340) by @Tobbe
  • fix(deploy): Point Render's health check at /graphql/health (#2314) by @Tobbe
  • fix(web): Make Cell props reflect beforeQuery's annotated parameter type (#2351) by @Tobbe
  • fix(auth-providers): Convert auth-*-web and auth-*-middleware packages to ESM-only (#2234) by @Tobbe
  • fix(graphql-server): Resolve auth state only at the entry point (#2279) by @Tobbe
  • fix(e2e): record report files before jobsWorkoff, diff after (#2245) by @lisa-assistant
  • fix(render): put sqlite deploy option on a paid plan, not free (#2404) by @lisa-assistant
  • fix(mailer-renderer-react-email): Convert @cedarjs/mailer-renderer-react-email to ESM-only (#2219) by @Tobbe
  • fix(api-server): Support server files in `cedarjs-server api`, refuse elsewhere (#2318) by @Tobbe
  • fix(fastify-web,cli-data-migrate,cli-storybook-vite): Convert remaining CJS-only packages to ESM-only (#2227) by @Tobbe
  • fix(mailer-handler-in-memory): Convert @cedarjs/mailer-handler-in-memory to ESM-only (#2212) by @Tobbe
  • fix(cli): Skip update check for non-semver @cedarjs/core specs (#2176) by @Tobbe
  • fix(ci): capture Playwright traces on failure for smoke tests (#2273) by @lisa-assistant
  • fix(fastify-web): Add cache headers and compression to the web server (#2327) by @Tobbe
  • fix(ci): disable Windows Defender real-time scanning on Windows runners (#2272) by @lisa-assistant
  • fix(graphql): Fail loudly when GraphQL options cannot be extracted (#2308) by @Tobbe
  • fix(mailer-core): Convert @cedarjs/mailer-core to ESM-only (#2211) by @Tobbe
  • fix(api-server,web-server): Default host to :: for dual-stack binding (#2337) by @Tobbe
  • fix(api): Always pass authDecoder for graphql requests (#2271) by @Tobbe
  • fix(create-cedar-app): switch api tsconfig to bundler module resolution (#2435) by @lisa-assistant
  • fix(gql): Trusted Documents 500s on every non-persisted request (auth is broken) (#2458) by @ladderschool
  • fix(cli): auto-start jobs worker in `cedar dev` when jobs are configured (#2442) by @lisa-assistant
  • fix(test): avoid race in prerender rehydration waitForResponse checks (#2276) by @lisa-assistant
  • fix(api-server): Drop @cedarjs/internal from production installs (#2313) by @Tobbe
  • fix(git-hooks): scope pre-push lint to branch-changed files only (#2247) by @lisa-assistant
  • fix(mailer-handler-resend): Convert @cedarjs/mailer-handler-resend to ESM-only (#2216) by @Tobbe
  • fix(cli): Remove old/stale webpack references (#2260) by @Tobbe
  • fix(mailer-handler-nodemailer): Convert @cedarjs/mailer-handler-nodemailer to ESM-only (#2215) by @Tobbe
  • fix(web-server): Convert @cedarjs/web-server to ESM-only (#2207) by @Tobbe
  • fix(cli): don't quote prisma option values containing spaces (#2125) by @Tobbe
  • ⚠️ fix(auth-providers)!: Convert auth-*-api and auth-*-setup packages to ESM-only (#2223) by @Tobbe
  • fix(mailer-renderer-mjml-react): Convert @cedarjs/mailer-renderer-mjml-react to ESM-only (#2218) by @Tobbe
  • fix(ci): Make the create-cedar-rsc-app install retry actually retry (#2195) by @Tobbe
  • fix(router): make PrivateSet discoverable by name and by common alias (#2330) by @lisa-assistant
  • fix(dbAuth): mention PrivateSet in setup post-install notes (#2361) by @lisa-assistant
  • fix(mailer-handler-studio): Convert @cedarjs/mailer-handler-studio to ESM-only (#2217) by @Tobbe
  • fix(vite): Resolve api/ bare specifiers without workspace symlinks (#2177) by @Tobbe
  • fix(cli): pretty-print api logs when running yarn cedar dev --ud (#2140) by @Tobbe
  • fix(ci): map the windows output through the detect-changes job (#2124) by @Tobbe
  • ⚠️ fix(build)!: Rename plugin that wrapps requests with AsyncLocalStorage (#2062) by @Tobbe
  • fix(vite): Reject $api imports from client-side code (#2263) by @Tobbe
  • fix(vite): forward dev server options via --fwd (#2356) by @lisa-assistant
  • fix(api-server): treat AbortError as client disconnect, return 499 (#2363) by @Tobbe
  • fix(cli): wrap scaffolded routes in PrivateSet when auth is set up (#2332) by @lisa-assistant
  • fix(ci): Make detect-changes robust to force-pushes via SHA comparison (#2121) by @Tobbe
  • fix(vite): Make cedar dev --ud shut down promptly on SIGINT/SIGTERM (#2197) by @Tobbe
  • fix(cli): guard Create/Update inputs behind crud in sdl templates (#2364) by @Tobbe
  • fix(testing): resolve jest moduleNameMapper paths instead of assuming hoisting (#2155) by @Tobbe
  • fix(api): Only build server auth state when an auth decoder is given (#2270) by @Tobbe
  • fix(ci): Split milestone check and assignment into single-trigger workflows (#2248) by @Tobbe
  • fix(tui): Convert @cedarjs/tui to ESM-only (#2205) by @Tobbe
  • fix(pm): jest+msw pnpm support (#2153) by @Tobbe
  • fix(vite): merge user babel plugins with react-compiler plugin (#2157) by @lisa-assistant
  • fix(git-hooks): stop pre-push lint from racing build's dist output (#2143) by @Tobbe
  • fix(cli): pass vite build bin path as argv array in buildHandler (#2127) by @Tobbe
  • fix(vite): resolve two correctness issues in buildApp's cedar-api-src-redirect (#2111) by @Tobbe
  • fix(api): Keep passing lambda events to getCurrentUser (#2166) by @Tobbe
  • fix(testing): Start MSW for web-side tests in ESM/Vitest apps (#2183) by @Tobbe
  • fix(cli): Include packages/* workspaces when running tests (#2439) by @Tobbe
  • fix(context,gqlorm,cli-helpers,internal,vite): Convert Tier 1 Dual Mode packages to ESM-only (#2237) by @Tobbe
  • fix(eslint-plugin): Convert @cedarjs/eslint-plugin to ESM-only (#2206) by @Tobbe
  • fix(framework-tools): atomically rewrite package.json in generateTypesCjs (#2120) by @Tobbe
  • ⚠️ fix(vite)!: replace buffer polyfill with data-uri-to-buffer shim (#2054) by @Tobbe
  • fix(project-config): update page count in paths test for AggregatedBlogPostPage (#2114) by @lisa-assistant
  • fix(structure): Detect Private/PrivateSet ancestors beyond the immediate parent (#2379) by @Tobbe
  • fix(testing): recognize Node-style MODULE_NOT_FOUND errors in MockProviders (#2117) by @lisa-assistant
  • fix(cli): Remove dead code from sdl generator (#2161) by @Tobbe
  • fix(cca): Switch app template start scripts to the cedarjs-server bin (#2323) by @Tobbe
  • fix(testing): fail fast when test DB URL doesn't match schema provider (#2329) by @lisa-assistant
  • fix(eslint-config): Update legacy config to use eslint-plugin-import-x (#2243) by @Tobbe
  • fix(mailer): Allow async renderers and upgrade @react-email/render to 2.1.0 (#2447) by @ladderschool
  • ⚠️ fix(babel)!: Remove getCommonPlugins() (#2165) by @Tobbe
  • fix(codegen): Surface client preset (Trusted Documents) generation errors (#2459) by @ladderschool
  • fix(vite): GraphQL Options extraction plugin sourcemaps (#2094) by @Tobbe
  • fix(prerender): Only emit the Apollo state script when there is state (#2446) by @ladderschool
  • fix(build): rewrite `.ts` imports (#2470) by @Tobbe
  • fix(cli): Make `upgrade --force` actually force the upgrade (#2477) by @Tobbe
  • fix(cli): Run the major version upgrade script for release candidates (#2478) by @Tobbe
  • fix(upgrade-scripts): Don't drop the generator-templates headline (#2480) by @Tobbe
  • fix(windows): Quote spawn arguments so paths with spaces work (#2481) by @Tobbe
  • fix(git-hooks): clear error when node_modules missing (e.g. worktrees) (#2484) by @lisa-assistant

📚 Docs

Click to see all 22 docs changes
  • docs(4.x): Remove leftover conflict markers from #1707 (0d277f6) by @Tobbe Lundberg
  • docs(cells): Add fragment Cells implementation notes (479c217) by @Tobbe Lundberg
  • docs(overview): mention fragment Cells (#2432) by @Tobbe
  • docs(deploy): Railway does two services by default (#2408) by @Tobbe
  • docs(deploy): Simplify Railway deployment docs (#2400) by @Tobbe
  • docs(deploy): Add server file note to any-container-host etc docs (#2373) by @Tobbe
  • docs(deploy): Serve tiers, topologies, and container-host conventions (#2338) by @Tobbe
  • docs(deploy): Clarify start:web's role and apiUrl vs apiProxyTarget (#2375) by @Tobbe
  • docs(deploy): tighten up Railway guide (#2456) by @Tobbe
  • docs(render): db migration (#2401) by @lisa-assistant
  • docs(deploy): Not really zero-config (needs db migrations) (#2402) by @Tobbe
  • docs(ci): Document new UD test flake — leaked servers then EADDRINUSE (#2196) by @Tobbe
  • docs(create-cedar-app): note that browser.open only applies interactively (#2358) by @lisa-assistant
  • docs(tutorial): Update yarn commands to `yarn cedar` (#2387) by @Tobbe
  • docs(rebrand): Update yarn commands to `yarn cedar` (#2388) by @Tobbe
  • docs(v4): Squash v4 docs into 4.x (#2267) by @Tobbe
  • docs(vite): Modernize Vite config page, promote plugin composability (#2172) by @Tobbe
  • docs(release): v6 upgrade guide (#2474) by @Tobbe
  • docs(v6): Read through and improve release docs (#2482) by @Tobbe
  • docs(ci): Record the silent serve-smoke-test failure, add diagnostics (#2483) by @Tobbe
  • docs(release): Tell people to pin vite 7.3.6, not 7.3.5 (#2493) by @Tobbe
  • docs(upgrade): v6 TOC tweaks and file rename (0c2619b) by @Tobbe Lundberg

📦 Dependencies

Click to see all 105 dependency updates

🧹 Chore

Click to see all 89 chore contributions
  • chore: update package versions to v6.0.0 (310408a) by @Tobbe Lundberg
  • chore(README): Update roadmap (2c95430) by @Tobbe Lundberg
  • chore(structure): use .api/ for apiUrl in cedar.toml (eefe850) by @Tobbe Lundberg
  • chore(merge): Fix broken merges into the release branch (d8b0588) by @Tobbe Lundberg
  • chore(docs): code-style taste (4a48b1f) by @Tobbe Lundberg
  • docs(plans): prerender rewrite update (5e3e021) by @Tobbe Lundberg
  • docs(plans): prerender rewrite (11574f2) by @Tobbe Lundberg
  • chore(docs): Move prerender plan from -docs to -plans (57c0b90) by @Tobbe Lundberg
  • chore(jsdocs): Minor jsdoc update to otel plugin (73f926d) by @Tobbe Lundberg
  • chore(api-server): Tighten comment text in test (#2345) by @Tobbe
  • chore(framework-tools): retry package.json rename on Windows EPERM (#2347) by @lisa-assistant
  • chore(rename): 7 internal-only GraphQL plugin functions get Cedar naming (#2319) by @lisa-assistant
  • chore(rename): internal-only Redwood identifiers -> Cedar (#2315) by @lisa-assistant
  • chore(esm): Convert cookie-jar, server-store, and record to ESM-only (#2258) by @Tobbe
  • docs(plan): Make logger config explicit (#2281) by @Tobbe
  • chore(api-server): Use knip to clean up dependencies (#2238) by @Tobbe
  • chore(knip): `@cedarjs/api-server-watch` (#2320) by @Tobbe
  • chore(vite): Extract UD plugin template strings into real functions (#2280) by @Tobbe
  • chore(cca): Remove redundant `dns.setDefaultResultOrder` from app templates (#2316) by @Tobbe
  • chore(vite): Serialize virtual functions from real functions (#2164) by @Tobbe
  • chore(api): Drop a redundant multi-value query param override (#2278) by @Tobbe
  • chore(render): Modernize the Render blueprint (#2307) by @Tobbe
  • chore(ts): Remove stale @ts-ignore suppressions (#2262) by @Tobbe
  • chore(vite): Fix dependencies found by knip (#2252) by @Tobbe
  • chore(tooling): Rewrite to TypeScript (#2145) by @Tobbe
  • chore(esm): Convert Tier 2 Dual Mode packages to ESM-only (#2241) by @Tobbe
  • chore(eslint-config): Remove unused dependencies found by knip (#2246) by @Tobbe
  • chore(telemetry): Convert @cedarjs/telemetry to ESM-only (#2204) by @Tobbe
  • chore(release): Add AI-agent DX section to v6 release notes (#2385) by @Tobbe
  • chore(auth-providers): Fix dependencies found by knip (#2255) by @Tobbe
  • chore(cca): engines.node: 24.x (#2311) by @Tobbe
  • chore(ci): include cli-helpers in the Windows always-run package list (#2126) by @Tobbe
  • docs(plan): sequencing plan (#2152) by @Tobbe
  • chore(test): Cover function routes receiving a stray auth-provider (#2274) by @Tobbe
  • docs(plans): Note that router/web going ESM-only would unblock cookie-jar/server-store (#2240) by @Tobbe
  • docs(plan): Move prerender to Vite (#2103) by @lisa-assistant
  • chore(ci): More robust tag checking for RC publish (#2282) by @Tobbe
  • chore(ud-tests): Per-test ports and per-test process ownership (#2200) by @Tobbe
  • docs(plan): Correct the zero-config platform claim to two tiers (#2310) by @Tobbe
  • chore(prerender): Reuse cedarImportDirPlugin from @cedarjs/vite (#2269) by @Tobbe
  • chore(vite): Clean up api proxy host config + comment (#2306) by @Tobbe
  • chore(ci): tarsync and setup for pnpm and npm (#2158) by @Tobbe
  • chore(release): Start prepping tooling for v6 release (#2174) by @Tobbe
  • chore(esm): Convert Tier 3 Dual Mode packages to ESM-only (#2254) by @Tobbe
  • chore(vite): wire vite-tsconfig-paths into buildApiWithVite and apiDevMiddleware (#2109) by @Tobbe
  • chore(jobs): use `import ... with { ...` (#2225) by @Tobbe
  • chore(internal): Configure knip for the internal package (#2324) by @Tobbe
  • chore(vite): Exclude babel transforms vite already handle (#2096) by @Tobbe
  • chore(ci): Fix storybook-framework-cedarjs RC publishing (#2159) by @Tobbe
  • chore(cli): Propagate cfw script failures instead of swallowing them (#2179) by @Tobbe
  • chore(vite): Simplified Babel config for Vite projects (#2160) by @Tobbe
  • chore(graphql-server): Configure knip for the graphql-server package (#2328) by @Tobbe
  • chore(vite): fully gate web-side babel-plugin-module-resolver on !forVite (#2138) by @Tobbe
  • chore(ud-tests): Fail fast and retry when a V8 debugger bug kills the dev server (#2201) by @Tobbe
  • chore(vite): Don't build ESM-only bin related files for CJS (#2144) by @Tobbe
  • chore(vite): Use vite-plugin-graphql-tag (#2087) by @Tobbe
  • chore(rsc): Set up from sources in CI (#2268) by @Tobbe
  • chore(vite): port babel-plugin-redwood-directory-named-import to vite (#2089) by @lisa-assistant
  • chore(rename): use __cedar_ for ALS-related internal names (#2074) by @Tobbe
  • chore(ci): Diagnostics for the silent Nx failures on Windows (#2192) by @Tobbe
  • chore(cli): Configure knip for the cli package (#2321) by @Tobbe
  • chore(ci): Run ESM smoke tests under pnpm and npm (#2180) by @Tobbe
  • chore(vite): replace babel-plugin-module-resolver ESM extension rewriting with applyEsmExtensions (#2116) by @lisa-assistant
  • chore(babel): babel-plugin-cedar-graphql-options-extract to Vite (#2070) by @lisa-assistant
  • docs(plans): prerender, streaming, rsc (#2132) by @Tobbe
  • chore(core): Remove unused dependencies found by knip (#2249) by @Tobbe
  • chore(vite): Use Vite import-dir plugin in buildApp (#2090) by @lisa-assistant
  • chore(ci): Flatten job graph, consolidate micro-jobs, filter Windows runs (#2122) by @Tobbe
  • chore(test-project): Postcss 8.5.16 Autoprefixer 10.5.2 (#2119) by @Tobbe
  • chore(record): Fix async test warning (#2136) by @Tobbe
  • chore(ci): Create test projects in paths with spaces (#2123) by @Tobbe
  • chore(internal): Dedicated job path injection transform for api builds (#2162) by @Tobbe
  • chore(vite): remove duplicate $api resolve.alias from getMergedConfig (#2142) by @Tobbe
  • chore(vite): port babel-plugin-redwood-directory-named-import to API build (#2108) by @Tobbe
  • chore(esm): Convert all but two CJS-only packages to ESM-only (#2239) by @Tobbe
  • chore(vite): Remove babel-plugin-module-resolver (#2079) by @Tobbe
  • chore(vite): port babel-plugin-redwood-otel-wrapping to Vite (#2073) by @lisa-assistant
  • ⚠️ chore(project-config)!: remove deprecated generators path (#1905) by @lisa-assistant
  • docs(plans): Full ESM migration plan (#2210) by @Tobbe
  • chore(babel): Clean up babel-plugin-auto-import usage (#2085) by @Tobbe
  • chore(vite): port babel-plugin-redwood-mock-cell-data to vite (#2072) by @lisa-assistant
  • chore(release): Stage tagging releases (#2147) by @Tobbe
  • chore(ci): Add better trusted documents coverage (#2462) by @Tobbe
  • chore(release): Update v6 release notes highlights and breaking changes (#2463) by @Tobbe
  • chore(cli): Tidy up upgrade command implementation (#2472) by @Tobbe
  • chore(ci): recognize refactor(scope) as a conventional commit prefix (#2473) by @lisa-assistant
  • chore(ci): better telemetry coverage (#2487) by @lisa-assistant
  • chore(storybook): Remove unused vite-plugin-node-polyfills (#2492) by @Tobbe
  • chore(cca): Remove base template lock files (8fff3dd) by @Tobbe Lundberg