Release Notes
Prisma typedSql support
It's now possible to configure parameters to pass to prisma generate when a new client is generated. This makes it possible to pass --sql, which is needed for typedSql support.
See more here: #1769
yarn cedar setup neon
Switch from the default sqlite database to a Neon Postgres database. It's free to use. No signup required. Just run the setup command and it will provision a database for you ready to use.
Even if you (eventually) want to use another Postgres provider, this is a pretty easy way to configure everything to get Prisma and your app set up for Postgres.
CEDAR_DELAY_API_RESTART
I've said this before, but I'm preparing to remove the RWJS_DELAY_RESTART environment variable. In this release I introduced a warning if that env var is detected. In the rare event you used this (previously undocumented) environment variable, please use CEDAR_DELAY_API_RESTART instead.
Changelog
🚀 Features
feat(ud): Register routes with UD using addEntry (#1723) by @Tobbe
This PR adds per-route addEntry() UD registrations. Each discovered API function (GraphQL, auth, health, regular functions) gets its own virtual module and UD store entry
feat(ud): Phase 5 - one Vite process (#1718) by @Tobbe
Use only one Vite process for building both web and api
feat(codemods): Prisma v7 tsconfig module (#1729) by @Tobbe
Set "module": "node20" in api/tsconfig.json to allow require(esm)
feat(cli): Add `prisma generate` args to cedar.toml (#1769) by @Tobbe
Allow setting "prismaGenerateArgs": ["--sql"] in cedar.toml, which will be used when Cedar internally generates the Prisma Client
🛠️ Fixes
fix(build): Restore v4 backwards compatibility (#1720) by @Tobbe
Cedar v4.1.0 accidentally broke v4 compatibility. This PR tries to bring it back. So users can go from v4.0.0 to v4.2.0 without needing any code or tooling changes
fix(api): support Fetch Request in getAuthProviderHeader (#1750) by @bellcoTech
Problem
Bearer-token auth silently fails when an @cedarjs/api handler is invoked via the Web Standard / middleware path (a Fetch Request rather than a Lambda APIGatewayProxyEvent). A request with valid auth-provider and Authorization: Bearer … headers comes back as unauthenticated, and getAuthenticationContext returns undefined — so any auth provider that uses Bearer tokens (Auth0, Supabase non-cookie, Clerk JWT, custom JWT, etc.) is broken on that path.
Root cause
getAuthProviderHeader (in packages/api/src/auth/index.ts) does:
Object.keys(event?.headers ?? {}).find(
(key) => key.toLowerCase() === AUTH_PROVIDER_HEADER,
)For a Lambda event, event.headers is a plain object, so Object.keys returns the header names. For a Fetch Request, event.headers is a Headers instance — Object.keys(headersInstance) returns [], the function returns undefined, and the short-circuit at the top of getAuthenticationContext decides the request is unauthenticated:
if (!typeFromHeader && !cookieHeader) {
return undefined
}So the Bearer-token code path is never even attempted on the middleware path.
Fix
When headers exposes a .get() method (the Headers API), look the value up directly via headers.get('auth-provider'). Fall back to the existing plain-object iteration for Lambda callers, so existing handlers behave exactly as they do today.
Surface area is intentionally tiny — one function, one file — and the Lambda path is the explicit fallback, so the change is additive rather than a behaviour swap.
Checks
Per CONTRIBUTING.md:
yarn check— clean (no constraint or dedupe issues)yarn testinpackages/api— 245 passing, including a new regression test inpackages/api/src/auth/__tests__/getAuthenticationContext.test.tsthat exercisesRequest+auth-providerheader +Authorization: Bearer …yarn buildinpackages/api— clean (ESM + CJS)yarn lint— clean (via pre-commit hook)
No new dependencies.
fix(ud): Build order and function fallback (#1725) by @Tobbe
Fix build order so that the ud output isn't overwritten
Support both handleRequest and handler, where the latter is wrapped by wrapLegacyHandler()
fix(cca): Yarn ESM overlay package.json deps and resolutions (#1712) by @Tobbe
These changes should have been part of #1673
fix(ud): Integrate UD's vite plugin (#1755) by @Tobbe
Add @universal-deploy/vite the Cedar's internal array of vite plugins. Let UD handle virtual module generation etc
fix(ud): Update the UD serv help message (#1728) by @Tobbe
The underlying tech for UD isn't important for the help message
fix(cli): Detect already generated prisma client (#1767) by @Tobbe
The Cedar cli tried to detect if a Prisma Client had already been generated or not. If it had, generation was skipped.
The problem was that the check was broken. It checked for CJS syntax when Prisma was actually using ESM syntax.
This broken check means the client was always regenerated every time the dev server was started. Turns out this was actually good. Otherwise schema changes wouldn't be reflected in the client.
I've now switched to a hash-based approach instead. If the computed hash for the prisma config and schema files changes, a new client needs to be generated. If the hash is the same, the old client can be reused.
fix(cli): collapse isLockSet existsSync/statSync race (#1753) by @bellcoTech
Problem
cedar build (and any other command that goes through setLock / isLockSet — including the update-check middleware that runs on most CLI commands) can crash with:
Error: ENOENT: no such file or directory, stat '.cedar/locks/<id>'
at Object.statSync (node:fs:1626:25)
at isLockSet (.../@cedarjs/cli/dist/lib/locking.js:…)
at setLock (.../@cedarjs/cli/dist/lib/locking.js:…)
We hit it consistently on Vercel deploys, where the build cache restores .cedar/locks/<id> as a dangling symlink — but the underlying race exists on any host. The error propagates up through every caller (there is no try/catch around setLock in the CLI plumbing) and aborts the build before it produces any output.
Root cause
isLockSet (in packages/cli/src/lib/locking.ts) is a classic check-then-act:
const exists = fs.existsSync(lockfilePath)
if (!exists) {
return false
}
const createdAt = fs.statSync(lockfilePath).birthtimeMs // ← can throw ENOENTThe two filesystem calls aren't atomic, so the lockfile can disappear between them in two realistic ways:
- Concurrent unset. Another process unsets the lock between the
existsSyncand thestatSync.unsetLockin this same file already wraps itsfs.rmSyncin a try/catch that swallows ENOENT for exactly this reason, butisLockSetdoesn't. - Broken symlink restored by a build cache. Vercel's incremental builds restore
.cedar/locks/<id>as a dangling symlink.existsSyncreturnstrue(the symlink node itself exists), butstatSyncfollows the link and throws ENOENT because the target file is gone.
Either case throws straight through isLockSet, which is on the hot path of every long-running CLI command via setLock and the update-check middleware. The result is a build that fails on the very first lock query.
Fix
Drop the existsSync pre-check and statSync directly, treating ENOENT as "not locked" and re-throwing anything else. This is exactly the pattern unsetLock in the same file already uses for the analogous rmSync call, so it lines up with the existing style and pairs naturally with the existing isErrorWithCode helper:
let createdAt: number
try {
createdAt = fs.statSync(lockfilePath).birthtimeMs
} catch (error) {
if (isErrorWithCode(error, 'ENOENT')) {
return false
}
throw error
}
if (Date.now() - createdAt > 3600000) {
unsetLock(identifier)
return false
}
return trueReal fs errors that aren't ENOENT — permission errors, EIO, etc. — are still surfaced unchanged, so the lock layer doesn't paper over genuine filesystem problems.
Test changes
locking.test.ts and updateCheck.test.ts both had a fs.statSync mock of the form:
fs.statSync = vi.fn(() => ({ birthtimeMs: Date.now() }))…with the comment "Prevent the appearance of stale locks". That mock returned a fresh birthtimeMs for every call, regardless of whether the lockfile actually existed in the underlying memfs filesystem — which only worked because the production code happened to gate on existsSync first. Now that statSync is the gating call, the mock has to mirror real fs semantics:
fs.statSync = vi.fn((lockfilePath) => {
if (!vol.existsSync(lockfilePath as string)) {
const error = new Error(
`ENOENT: no such file or directory, stat '${lockfilePath}'`,
) as NodeJS.ErrnoException
error.code = 'ENOENT'
throw error
}
return { birthtimeMs: Date.now() }
})Same change applied in both files. Without it the updateCheck tests would assume the lock is always set, which masks the bug being fixed here.
Two new regression tests in locking.test.ts cover the production code paths:
Returns false when statSync throws ENOENT— simulates the broken-symlink / concurrent-unset race directly.Re-throws non-ENOENT statSync errors— guards against the obvious over-correction of swallowing all errors.
Checks
Per CONTRIBUTING.md:
yarn check— clean (no constraint or dedupe issues)yarn test src/lib/__tests__/locking.test.ts src/lib/__tests__/updateCheck.test.tsinpackages/cli— 30 passing (11 locking + 19 updateCheck)yarn buildinpackages/cli— cleanyarn lint— clean (via pre-commit hook)
No new dependencies. Pre-existing failures in unrelated CLI tests (cwd.test.js) reproduce on main and are not introduced by this PR.
fix(cli): replace unknown-as cast in prerender flatMap with proper return type (#1697) by @mvanhorn
Fixes #1626.
What changed
In packages/cli/src/commands/prerenderHandler.ts, the routesToPrerender.flatMap callback used a type lie to satisfy TypeScript:
return [] as unknown as { title: string; task: () => Promise<void> }Replaced with an explicit return type on the callback, so both branches return arrays that flatMap flattens normally:
type PrerenderTask = { title: string; task: () => Promise<void> }
return routesToPrerender.flatMap((routeToPrerender): PrerenderTask[] => {
if (routerPathFilter && routeToPrerender.path !== routerPathFilter) {
return []
}
...
return [{ title: ..., task: async () => { ... } }]
})Also removed the TODO comment and the dangling link to a redwoodjs/graphql PR; the cast was the entire reason that comment block existed.
Why this matters
as unknown as is the strongest type assertion TypeScript offers — it silences any error, including ones that would catch real bugs in the same file. Here it was hiding nothing dangerous (flatMap does the right thing with []), but the pattern is easy to copy-paste into spots where it's not safe. Replacing it with an explicit callback return type closes the door without changing behavior.
Verification
flatMap([] | [{...}])flattens to[{...}]— same shape Listr2 received before.- Diff is contained to one function. No call sites change; the resulting
listrTasksarray still has the same element type.
fix(prisma): Explain schema.prisma config settings (#1761) by @Tobbe
Had this comment first, but it felt like it was too long
// For CJS Cedar apps:
// It might feel a little out of place to have .mts files in the codebase, and
// especially importing them like we do in `src/lib/db`.
// For ESM Cedar apps:
// It might feel a little weird to ask Prisma to generate CJS compatible format.
//
// Setting `moduleFormat = "cjs"` and using `.mts` extensions is however the
// only combination that makes Prisma 7, which is ESM-only, work in CJS Cedar
// apps while also having the same configuration work for ESM Cedar apps.
//
// The only reason this works is thanks to Node 24's support for require(esm).
// https://www.prisma.io/docs/orm/prisma-schema/overview/generatorsSo I shortened it to what we have now, and then pointing here for more info.
This all came out of the discussion on PR #1752
📚 Docs
docs(tutorial): fix chapter 1 — CLI commands, callouts, app name (#1705) by @lisa-assistant
Changes
yarn redwood → yarn cedar across all chapter 1 pages:
yarn redwood generate page home /→yarn cedar generate page home /yarn redwood generate page about→yarn cedar generate page aboutyarn redwood g layout blog→yarn cedar g layout blogyarn redwood dev→yarn cedar dev- All prose references to the
redwoodCLI updated tocedar
Tutorial app renamed: redwoodblog → cedarblog
Discourse link removed from prerequisites (was linking to the RedwoodJS community forum), Discord link kept.
Broken :::info callouts fixed: MDX 3 requires :::info[Title] (bracket syntax) for titled admonitions — the old :::info Title (space syntax) renders as literal text. Fixed in:
prerequisites.md— "Installing Node and Yarn"first-page.md— "Automatic import of pages in the Routes file"second-page.md— "Code-splitting each page"layouts.md— "Thesrcalias" and "Why are things named the way they are?"
docs(tutorial): replace stale version callout with Discord tip (#1703) by @lisa-assistant
The callout saying "This tutorial assumes you are using version 0.1.0 or greater of CedarJS" was outdated — CedarJS is now on 3.x.
Rather than just removing it, this replaces it with a tip pointing readers to the CedarJS Discord if they get stuck — which is genuinely useful in a tutorial context and still demonstrates the callout concept.
docs(tutorial): What is CedarJS updates (#1706) by @Tobbe
Follow-up to #1704, addressing a few more outdated sections
docs(tutorial): fix outdated content in 'What is CedarJS?' (#1704) by @lisa-assistant
Several pieces of the "What is CedarJS?" page were inherited from RedwoodJS and are no longer accurate.
Changes
- Tech list:
Vitest→Jest(default Cedar apps use Jest; only the ESM templates use Vitest) - Storybook command:
yarn redwood storybook→yarn cedar storybook - Build tooling: "compiled by Babel" → "compiled by esbuild" for the api side; section heading updated accordingly
- Coming Soon: removed broken
redwoodjs.com/roadmaplink and "Bighorn Epoch" reference (a RedwoodJS concept); replaced with a link togithub.com/cedarjs/cedar#roadmap - Backing: clarified that CedarJS is a fork of RedwoodJS — it was not created by Tom Preston-Werner
- Community: removed "very active" claim, removed Discourse forum link (RedwoodJS resource), kept Discord link only
docs(plans): DB module (#1700) by @Tobbe
Two competing/complimentary plans on how to support moving the db code into its own package
One plan on how to improve gqlorm to not assume a db path, and also not depend on a cedar.toml setting for the db path
docs(tutorial): fix chapter 2 — CLI names, callouts, and factual errors (#1713) by @lisa-assistant
Summary
Fixes CedarJS tutorial chapter 2 (Getting Dynamic, Cells, Routing Params) inherited from RedwoodJS:
- CLI commands: Replace all
yarn rwandyarn redwoodwithyarn cedar - Remove
rwalias tip: CedarJS has norwalias — remove the tip block ingetting-dynamic.mdthat introduced it - Broken callouts: Fix all titled admonitions from
:::info Title(space syntax) to:::info[Title](bracket syntax) required by MDX 3 / Docusaurus 3 - SDL file extension:
posts.sdl.{jsx,ts}→posts.sdl.{js,ts}(SDL files are.js/.ts, not.jsx) - Example URLs:
redwoodblog.com→cedarblog.com
Files changed
docs/docs/tutorial/chapter2/getting-dynamic.mddocs/docs/tutorial/chapter2/cells.mddocs/docs/tutorial/chapter2/routing-params.md
Test plan
- Verify all callouts render with titles on the live docs site
- Verify no
yarn rworyarn redwoodreferences remain in chapter 2
🤖 Generated with Claude Code
docs(gqlorm): First revision (#1759) by @Tobbe
Updating the docs as I read them through now that they're published to https://cedarjs.com/docs/canary/graphql/gqlorm/
📦 Dependencies
Click to see all 15 dependency updates
- fix(deps): update prisma monorepo to v7.8.0 (#1695) by @renovate-bot
- chore(deps): update dependency @types/estree to v1.0.9 (#1732) by @renovate-bot
- chore(deps): update dependency @supabase/supabase-js to v2.105.1 (#1699) by @renovate-bot
- fix(deps): update dependency @swc/core to v1.15.32 (#1698) by @renovate-bot
- fix(deps): update dependency @faire/mjml-react to v3.5.4 (#1748) by @renovate-bot
- chore(deps): update pnpm to v10.33.4 (#1746) by @renovate-bot
- chore(deps): update pnpm to v10.33.2 (#1692) by @renovate-bot
- chore(deps): update yarn to v4.14.1 (#1673) by @renovate-bot
- chore(deps): update github/codeql-action digest to 68bde55 (#1730) by @renovate-bot
- fix(deps): update dependency @swc/core to v1.15.33 (#1768) by @renovate-bot
- chore(deps): update dependency publint to v0.3.19 (#1734) by @renovate-bot
- chore(deps): update github/codeql-action digest to e46ed2c (#1711) by @renovate-bot
- chore(deps): update dependency publint to v0.3.20 (#1758) by @renovate-bot
- chore(deps): update dependency eslint-plugin-package-json to v0.91.2 (#1715) by @renovate-bot
- fix(deps): update babel monorepo (#1747) by @renovate-bot
🧹 Chore
Click to see all chore contributions
- chore(test-project): Clean up old fixtures (f2b27bc) by @Tobbe
- Version docs to 4.2 (6229e11) by @Tobbe
- chore(yarn): Fix build after yarn 4.14.1 upgrade (#1721) by @Tobbe
- chore(test-projects): Bump postcss to ^8.5.13 (#1701) by @Tobbe
- chore(cfw): remove legacy RWFW_PATH and RW_PATH env var fallbacks (#1696) by @lisa-assistant
- chore(vite): replace vite-node with Vite's native RunnableDevEnvironment (#1717) by @lisa-assistant
- chore(build): Use nx run-many instead of lerna run (#1738) by @Tobbe
- chore(cca): Remove unwanted yarn.lock files (#1722) by @Tobbe
- chore(ci): Update flaky windows CI analysis doc (root cause found) (#1763) by @Tobbe
- chore(release): Fix canary release script (#1743) by @Tobbe
- chore(ci): Cache prebuild entries (#1742) by @Tobbe
- chore(ud): Add e2e tests (#1726) by @Tobbe
- chore(ci): Work on improving flaky Windows playwright smoke tests (#1756) by @Tobbe
- chore(smoke-tests): Auth tests cleanup/fixes (#1741) by @Tobbe
- chore(docs): Move UD phase 5 handoff (#1719) by @Tobbe
- chore(ci): Fix Windows cache race condition (#1724) by @Tobbe
- chore(publish): Remove lerna (#1739) by @Tobbe
- chore(ci): fix create-cedar-rsc-app yarn install on Windows (#1745) by @Tobbe
- chore(ci): Allow postcss changes in fixtures (#1714) by @Tobbe
- chore(ci): Cache prebuild entries - check if path exists (#1744) by @Tobbe
- chore(smoke-tests): Merge auth smoke tests with dev (#1740) by @Tobbe
- chore(local-testing-project): Update to Cedar v4.1.0 (#1757) by @Tobbe
- chore(lefthook): proseWrap new md files (#1754) by @Tobbe
- chore(ci): Explain usage of pull_request_target (#1760) by @Tobbe
- chore(api-server): warn when deprecated RWJS_DELAY_RESTART env var is used (#1764) by @lisa-assistant
