Skip to content

v4.1.0

Choose a tag to compare

@Tobbe Tobbe released this 30 Apr 01:00
· 2457 commits to main since this release

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(gqlorm): TS type gen (#1619) by @Tobbe

Generate types for useLiveQuery

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 β€” new CedarHandler, CedarRequestContext, CedarMiddleware, and CedarRouteRecord types; ctx.query is URLSearchParams rather than a flat record
  • lambdaLoader β€” now dispatches to either a legacy handler or a new handle(request, ctx) export; existing functions keep working unchanged
  • graphql.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:

  1. Web Vite server on port 8910 β€” standard Vite dev server for the frontend
  2. 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 8911

Instead 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_FUNCTIONS registry
  • Routes are registered dynamically:
    • /graphql and /graphql/* β†’ handled by GraphQL Yoga
    • /:routeName and /: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 api or only web is passed as a workspace
  • Streaming SSR is enabled (it has its own cedar-dev-fe server in devFeServer.ts)
  • A custom server.js file exists
  • Either api/src or web/src is 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 calls generateGqlormArtifacts() when experimental.gqlorm.enabled is set
  • watch.ts β€” same guard on the Prisma schema file watcher path
  • gqlormSchema.ts β€” early return at the top of generateGqlormArtifacts() as belt-and-suspenders; also cleans up any stale backend.ts if 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

🧹 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