v4.1.0
Release Notes
cedar-gen
Public:
rw-gen -> cedar-gen
Internal:
REDWOOD_ENV_FILES_LOADED -> CEDAR_ENV_FILES_LOADED
If you used the rw-gen bin directly, you'll now have to run cedar-gen instead. And if you used to read or write the REDWOOD_ENV_FILES_LOADED env you now have to switch the the Cedar equivalent instead.
Please let me know if you're affected by any of these changes as I'd like to better understand your usecase.
ctx.query is now a URLSearchParams
This is only relevant if you've started playing with Cedar's new web-compatible functions.
Cedar's handleRequest function receives ctx.query as a standard web-platform
URLSearchParams
instead of a flat Record<string, string>. This gives you multi-value support,
iteration, and serialisation for free β no extra dependencies needed.
Migrating from the legacy handler
Legacy handler functions receive event.queryStringParameters parsed by
picoquery, which expands bracket notation into arrays and nested objects. The
table below shows the equivalent using ctx.query.
| Raw query string | Legacy handler |
Cedar handle |
|---|---|---|
?page=1 |
event.queryStringParameters.page β '1' |
ctx.query.get('page') β '1' |
?tag=cedar&tag=framework |
event.queryStringParameters.tag β ['cedar', 'framework'] |
ctx.query.getAll('tag') β ['cedar', 'framework'] |
?ids[]=1&ids[]=2 |
event.queryStringParameters.ids β ['1', '2'] |
ctx.query.getAll('ids[]') β ['1', '2'] |
?user[name]=alice |
event.queryStringParameters.user.name β 'alice' |
ctx.query.get('user[name]') β 'alice' |
The key difference with bracket notation is that URLSearchParams preserves the
raw key name literally β ids[]=1 is stored under the key 'ids[]', not 'ids'.
For the common case of plain flat parameters the migration is a straightforward
swap: event.queryStringParameters.foo β ctx.query.get('foo').
Not ready to migrate? Use picoquery directly
If you rely heavily on bracket-notation parsing and can't migrate yet, you can
reproduce the legacy behaviour inside a handle function by parsing request.url
with picoquery yourself:
import { parse } from 'picoquery'
export const handle = async (request, ctx) => {
const rawSearch = new URL(request.url).search
const query = parse(rawSearch ? rawSearch.slice(1) : '', {
nestingSyntax: 'index',
arrayRepeat: true,
arrayRepeatSyntax: 'bracket',
})
// query.ids β ['1', '2'] for ?ids[]=1&ids[]=2
// query.user.name β 'alice' for ?user[name]=alice
// query.page β '1' for ?page=1
}Changelog
π Features
feat(gqlorm): Auth (#1623) by @Tobbe
This PR implements the backend auth model for gqlorm auto-generated resolvers.
Uses an "Organization" and "Membership" multitenant auth solution.
feat(api): introduce Fetch-native handler contract for Universal Deploy (#1616) by @Tobbe
Lays the groundwork for Universal Deploy integration by introducing a Fetch-native server contract alongside the existing Lambda model.
What's new
packages/api/src/runtime.tsβ newCedarHandler,CedarRequestContext,CedarMiddleware, andCedarRouteRecordtypes;ctx.queryisURLSearchParamsrather than a flat recordlambdaLoaderβ now dispatches to either a legacyhandleror a newhandle(request, ctx)export; existing functions keep working unchangedgraphql.ts/useRedwoodAuthContextβ wired to propagate Cedar request context through the GraphQL pipeline- A route manifest (
CedarRouteRecord[]) is built at load time, enumerating API functions by type β the foundation for generating platform-specific fetch handlers under Universal Deploy - Implementation plan added at
docs/implementation-plans/universal-deploy-integration-plan-refined.md
No breaking changes
All new types and the handle export are additive. Existing handler exports continue to work exactly as before via wrapLegacyHandler.
feat(cli): Rename bins to cedar-* (#1666) by @Tobbe
This PR renames rw-* bins to cedar-*. I've tried to make it backwards compatible by only deprecating (and not fully removing/renaming) existing bins that end users would execute. Please let me know if you run into any issues and I'll fix it.
feat(api): expose protocol and host on lambda fastify requests (#1654) by @mgramigna
We have a use case where we need to re-construct a url in our lambda handler based on the event.
In a typical lambda handler, these properties are expected to be there, but cedar appears to not be mapping all required properties from the fastify request. We just need these ones, but worth considering if we should be trying to map more. This is the expected type from aws lambda:
export interface APIGatewayProxyEventBase<TAuthorizerContext> {
body: string | null;
headers: APIGatewayProxyEventHeaders;
multiValueHeaders: APIGatewayProxyEventMultiValueHeaders;
httpMethod: string;
isBase64Encoded: boolean;
path: string;
pathParameters: APIGatewayProxyEventPathParameters | null;
queryStringParameters: APIGatewayProxyEventQueryStringParameters | null;
multiValueQueryStringParameters: APIGatewayProxyEventMultiValueQueryStringParameters | null;
stageVariables: APIGatewayProxyEventStageVariables | null;
requestContext: APIGatewayEventRequestContextWithAuthorizer<TAuthorizerContext>;
resource: string;
}
export interface APIGatewayEventRequestContextWithAuthorizer<TAuthorizerContext> {
accountId: string;
apiId: string;
authorizer: TAuthorizerContext;
connectedAt?: number | undefined;
connectionId?: string | undefined;
domainName?: string | undefined;
domainPrefix?: string | undefined;
eventType?: string | undefined;
extendedRequestId?: string | undefined;
protocol: string;
httpMethod: string;
identity: APIGatewayEventIdentity;
messageDirection?: string | undefined;
messageId?: string | null | undefined;
path: string;
stage: string;
requestId: string;
requestTime?: string | undefined;
requestTimeEpoch: number;
resourceId: string;
resourcePath: string;
routeKey?: string | undefined;
}feat(ud): Phase 2, 3 and 4 (#1663) by @Tobbe
I keep working towards universal-deploy support. To keep things runnable and verifiable this PR introduces a few temporary constructs that will be removed as we make progress. They're all clearly marked in both the overall plan document and in the code.
feat(cli): Build the api workspace using Vite (#1665) by @Tobbe
AI generated PR description:
Overview
The branch introduces a unified dev server (cedar-unified-dev) that replaces the previous two-process setup (Vite dev server + nodemon/esbuild API watcher) with a single process that runs two Vite servers:
- Web Vite server on port
8910β standard Vite dev server for the frontend - API Vite server on port
8911β Vite in SSR/middleware mode + Fastify for the backend
Entry Point
When you run cedar dev, the CLI handler (packages/cli/src/commands/dev/devHandler.ts) detects that both api and web workspaces are included (the default), and that neither Streaming SSR nor a custom server.js file is present. In that case, it uses a single command:
cedar-unified-dev --port 8910 --apiPort 8911Instead of the old fallback which started separate api and web processes via concurrently.
The API Dev Server (packages/vite/src/apiDevServer.ts)
This is the biggest change. Instead of esbuild + babel + nodemon + a forked child process, the API side now runs through Vite's SSR module runner:
1. Vite SSR Dev Server
- Created with
configFile: false,middlewareMode: true,appType: 'custom' - Loads API source files via
viteServer.ssrLoadModule(fileUrl) - Applies Babel transforms via a custom Vite plugin (
cedar-api-babel-transform) - Resolves workspace packages directly to source files via
resolve.alias
2. Fastify App
- A Fastify server runs in the same process as the Vite SSR server
- It registers
fastify-raw-body,fastify-url-data, and content-type parsers - API functions are loaded into an in-memory
LAMBDA_FUNCTIONSregistry - Routes are registered dynamically:
/graphqland/graphql/*β handled by GraphQL Yoga/:routeNameand/:routeName/*β handled by the lambda request handler
3. HMR (No More Nodemon)
- Vite's file watcher watches
api/src/ - On
change/add/unlink, it invalidates the affected modules in Vite's module graph and reloads all API functions - This means no process restart β changes are picked up via module invalidation
The Web Dev Server (packages/vite/src/cedar-unified-dev.ts)
The web side is still a standard Vite dev server, created with the user's web/vite.config.ts. The Vite config (packages/vite/src/lib/getMergedConfig.ts) still includes the proxy configuration:
server: {
proxy: {
[cedarConfig.web.apiUrl]: {
target: `http://${apiHost}:${apiPort}`,
changeOrigin: false,
rewrite: (path) => path.replace(cedarConfig.web.apiUrl, '')
}
}
}So API requests from the browser still hit the web Vite server on 8910, which proxies them to the Fastify API server on 8911.
The difference is what it proxies to
In the old approach:
Browser β Vite dev server (8910) β Proxy β API server process (8911, separate)
β nodemon + esbuild
In the new unified approach:
Browser β Vite dev server (8910) β Proxy β API server (8911, same process)
β Vite SSR + Fastify
The Wait-for-API Plugin
The default Cedar Vite plugin stack includes cedarWaitForApiServer() (packages/vite/src/plugins/vite-plugin-cedar-wait-for-api-server.ts). This middleware intercepts API requests and waits (up to 60s) for the API port to be open before letting them through. This prevents race conditions where the web server starts receiving API requests before the API server is ready.
Flow Summary
cedar dev
ββ> devHandler.ts detects unified mode
ββ> cedar-unified-dev
ββ> startApiDevServer(8911)
β ββ> Vite SSR dev server (middlewareMode, no HTML)
β ββ> Load api/src/functions/* via ssrLoadModule + Babel
β ββ> Setup HMR watchers (change/add/unlink β invalidate + reload)
β ββ> Fastify app listens on 8911
β
ββ> createServer({ configFile: web/vite.config.ts, port: 8910 })
ββ> Standard Vite dev server for web
ββ> Proxy /api/* β http://127.0.0.1:8911
ββ> cedarWaitForApiServer plugin guards API requests
What's Replaced
| Old | New |
|---|---|
| esbuild + babel for API build | Vite SSR module runner + Babel plugin |
| nodemon watching api/src | Vite's built-in file watcher + module invalidation |
| Forked child process for API | Same process: Vite SSR + Fastify |
api-server-watch binary |
startApiDevServer() function |
chokidar + manual rebuild |
Vite's HMR graph invalidation |
When It Falls Back
The unified dev server is not used when:
- Only
apior onlywebis passed as a workspace - Streaming SSR is enabled (it has its own
cedar-dev-feserver indevFeServer.ts) - A custom
server.jsfile exists - Either
api/srcorweb/srcis missing
In those cases, it falls back to the previous separate-process behavior.
feat(cli): API proxy path defaults to .api/functions (#1691) by @Tobbe
Switch from .redwood/functions to .api/functions as the default api proxy path
Only switches the default for new apps. No impact on existing apps
feat(gqlorm): Autogenerate backend (#1615) by @Tobbe
Uses codegen to produce the files needed to add gqlorm models to a Cedar app's graphql server
π οΈ Fixes
fix(api-server): use pathToFileURL instead of file:// string interpolation (#1622) by @Tobbe
Fixes the following warning when running unit tests
β― yarn workspace @cedarjs/api-server test
RUN v3.2.4 /Users/tobbe/dev/cedarjs/cedar/packages/api-server
11:25:19 AM [vite] (ssr) warning: File URL host must be "localhost" or empty on darwin
Plugin: vite:dynamic-import-vars
File: /Users/tobbe/dev/cedarjs/cedar/packages/api-server/src/plugins/lambdaLoader.ts
fix(storybook): explicitly add `tsconfig-paths` dependency (#1651) by @gameroman
Never trust implicit transitive dependencies.
fix(graphql-server): prefer x-request-id and fetch-native user-agent in GraphQL logger (#1621) by @Tobbe
Summary
When the GraphQL context carries a fetch-native Request object, read request IDs and user-agent strings directly from its headers rather than relying solely on the Lambda event shape.
Changes
requestId logging β checks x-request-id first (a widely adopted informal HTTP header used by load balancers, API gateways, and clients for distributed tracing), falls back to the Lambda/API-Gateway request ID, then generates a UUID as a last resort. This preserves an externally assigned trace ID end-to-end.
userAgent logging β reads request.headers.get('user-agent') as the primary source. Falls back to event.headers['user-agent'] for backwards compatibility with Lambda-only deployments.
Both changes are backwards-compatible: the Lambda fallbacks are preserved in full.
fix(realtime): Add better realtime support to prerender (#1683) by @Tobbe
Add better (but not full) realtime support to prerender and any other codepath that don't load a full Fastify server. And by "not full", I mean that you for example can't use @live queries (yet). They still error, but they do get further
fix(codegen): guard gqlorm generation behind experimental.gqlorm.enabled flag (#1677) by @lisa-assistant
Problem
[gqlorm] log messages were appearing during yarn dev and code generation in every Cedar app, even those that haven't enabled gqlorm:
[gqlorm] User.hashedPassword was automatically hidden because its name appears sensitive.
[gqlorm] User.salt was automatically hidden because its name appears sensitive.
generateGqlormArtifacts() was called unconditionally. The experimental.gqlorm.enabled config check only gated writing backend.ts β the schema-building step (which emits the warnings) always ran regardless.
Fix
Guard gqlorm artifact generation at every call site and inside the function itself:
generate.tsβ only callsgenerateGqlormArtifacts()whenexperimental.gqlorm.enabledis setwatch.tsβ same guard on the Prisma schema file watcher pathgqlormSchema.tsβ early return at the top ofgenerateGqlormArtifacts()as belt-and-suspenders; also cleans up any stalebackend.tsif the flag is turned off after having been enabled
Apps without experimental.gqlorm.enabled = true in cedar.toml will no longer see any gqlorm output.
Closes #1675
fix(api-server): Handler lookup guard (#1680) by @Tobbe
Fixing CodeQL comment that was raised here: https://github.com/cedarjs/cedar/pull/1665/checks?check_run_id=72928785791
It sounds more alarming than it really is. We already checked to make sure LAMBDA_HADLERS[handler] wasn't falsy but if someone tried to use toString as the handler name that'd pass the falsy check. This PR fixes that too
fix(api-server): lambda functions can't be undefined (#1682) by @Tobbe
Don't allow lambda functions to be undefined
π¦ Dependencies
Click to see all 33 dependency updates
- fix(deps): update dependency prettier to v3.8.2 (#1613) by @renovate-bot
- chore(deps): update dependency memfs to v4.57.2 (#1658) by @renovate-bot
- fix(deps): update dependency @fastify/http-proxy to v11.4.4 (#1659) by @renovate-bot
- chore(deps): update dependency nx to v22.6.5 (#1620) by @renovate-bot
- fix(deps): update dependency lru-cache to v11.3.5 (#1628) by @renovate-bot
- fix(deps): update dependency rollup to v4.60.2 (#1670) by @renovate-bot
- chore(deps): Bump postcss to 8.5.10 (#1646) by @Tobbe
- chore(deps): update dependency lefthook to v2.1.6 (#1657) by @renovate-bot
- fix(deps): update dependency react-hook-form to v7.73.1 (#1674) by @renovate-bot
- chore(deps): update dependency @prisma/dev to v0.24.7 (#1653) by @renovate-bot
- chore(deps): update dependency redis to v5.12.1 (#1672) by @renovate-bot
- chore(deps): Bump nx to 22.7.0 (#1687) by @Tobbe
- fix(deps): update dependency fastify to v5.8.5 (#1668) by @renovate-bot
- fix(deps): update dependency lru-cache to v11.3.3 (#1561) by @renovate-bot
- chore(deps): update dependency @auth0/auth0-spa-js to v2.19.2 (#1630) by @renovate-bot
- fix(deps): update dependency better-sqlite3 to v12.9.0 (#1629) by @renovate-bot
- fix(deps): update dependency type-fest to v5.6.0 (#1675) by @renovate-bot
- fix(deps): update dependency @swc/core to v1.15.30 (#1667) by @renovate-bot
- fix(deps): update dependency firebase-admin to v13.8.0 (#1608) by @renovate-bot
- chore(deps): update dependency @npmcli/arborist to v9.4.3 (#1689) by @renovate-bot
- chore(deps): update dependency @supabase/supabase-js to v2.103.3 (#1656) by @renovate-bot
- fix(deps): update graphqlcodegenerator monorepo (#1632) by @renovate-bot
- chore(deps): update dependency ora to v9.4.0 (#1681) by @renovate-bot
- fix(deps): update dependency @graphql-codegen/typescript-react-apollo to v4.4.2 (#1660) by @renovate-bot
- fix(deps): update typescript-eslint monorepo to v8.58.1 (#1587) by @renovate-bot
- chore(deps): update github/codeql-action digest to 95e58e9 (#1624) by @renovate-bot
- fix(deps): update dependency prettier to v3.8.3 (#1669) by @renovate-bot
- chore(deps): update actions/setup-node digest to 48b55a0 (#1652) by @renovate-bot
- fix(deps): update graphql-tools monorepo (#1693) by @renovate-bot
- fix(deps): update dependency react-hook-form to v7.74.0 (#1694) by @renovate-bot
- chore(deps): update dependency prettier-plugin-tailwindcss to v0.7.3 (#1690) by @renovate-bot
- chore(deps): update dependency @supabase/supabase-js to v2.104.1 (#1671) by @renovate-bot
- fix(deps): update dependency isbot to v5.1.39 (#1631) by @renovate-bot
π§Ή Chore
Click to see all chore contributions
- Version docs to 4.1 (8ac3322) by @Tobbe
- chore(storybook): use @cedarjs/testing MockRouter instead of local duplicate (#1643) by @lisa-assistant
- chore(gitignore): .nx/* (#1655) by @Tobbe
- chore(router): remove stale 'kick CI' comment from index.ts (#1641) by @lisa-assistant
- chore(docs): WinterTC (#1662) by @Tobbe
- chore(ci): rebuild test-project fixture in Renovate postUpgradeTasks (#1647) by @lisa-assistant
- chore(api-server): Rename new handler to handleRequest (#1661) by @Tobbe
- chore(ci): Fix inefficient logic in detectChanges (#1664) by @Tobbe
- chore(prettier): Re-enable prettier-plugin-sh (#1614) by @Tobbe
- chore(ci): Revert adding Renovate postUpgradeTasks (#1678) by @Tobbe
- chore(ci): Correct root package.json cedarjs package versions (#1609) by @Tobbe
- chore(cca): rename `overwrite` to `overwriteFlag` for consistency (#1640) by @lisa-assistant
- chore(build): Fix build config so the build is fully cacheable (#1685) by @Tobbe
- chore(nx): Fix build and test config so caching works (#1688) by @Tobbe
- chore(create-cedar-app): pin @prisma/adapter-pg version directly in overlay files (#1645) by @lisa-assistant
- chore(cli): Improvements and fixes to dev and build tests (#1686) by @Tobbe
- chore(api-server): Add comment belonging to #1680/#1682 (#1684) by @Tobbe
- chore(api-server): use terminalLink for route names in lambdaLoader output (#1644) by @lisa-assistant