Upgrading Beignet

Beignet packages version together. Upgrade every installed @beignet/* package, @beignet/cli, and create-beignet to the same release instead of mixing versions across the framework.

Upgrade workflow

  1. Read the target release notes and the migration section on this page.
  2. Update all Beignet packages to the same version and reinstall dependencies.
  3. Run bun beignet doctor --strict and bun beignet lint.
  4. Run the app's typecheck, test, and production build scripts.
  5. Exercise app-owned worker, cron, webhook, outbox, and task entrypoints that are not covered by the web build.

Generated files belong to the app after creation. Do not rerun create-beignet over an existing app or copy a fresh starter wholesale. Apply migration steps to the app's current structure and use doctor fixers only for findings explicitly marked safe.

Unreleased 1.0 stabilization changes

The current stabilization pass removes redundant pre-adoption APIs before the compatibility promise hardens.

CLI commands use one vocabulary

Provider setup now uses the singular provider command group and capability-first preset names. Replace beignet providers add redis-cache with beignet provider add cache-redis, and replace providers audit with provider audit. App creation now accepts the same catalog through --providers; the former --integrations flag is removed.

Workflow artifact names consistently use feature.name, including the value passed to make listener --event. The slash form is removed. make feature --with accepts singular addon names such as event, job, and upload.

Resource generation spells out --authorization and --tenant-scoped, and manual schedule runs use --run-id. Existing-app make, db, task, schedule, and outbox commands now accept --cwd <dir> consistently.

Runtime port wiring has an explicit name

The app's compile-time port shape remains in ports/index.ts. Rename the runtime wiring file and value so they cannot be confused with AppPorts:

mv infra/app-ports.ts infra/port-wiring.ts
// Before
import { appPorts } from "@/infra/app-ports";

// After
import { initialPorts } from "@/infra/port-wiring";

Pass initialPorts to the server's ports option and use it as the base in test context factories. Custom CLI config replaces paths.infrastructurePorts with paths.portWiring:

export default defineConfig({
  paths: {
    ports: "src/ports/index.ts",
    portWiring: "src/infra/port-wiring.ts",
  },
});

There is no compatibility alias for the former path, symbol, or config key.

Query schemas declare their URL transport

Every HTTP contract with a query schema now passes an explicit transport as the second .query(...) argument. This keeps Standard Schema validation and transforms separate from the URL representation shared by typed clients, servers, and OpenAPI.

// Before
listTodos.query(
  z.object({
    limit: z.coerce.number().int().optional(),
    tags: z.array(z.string()).optional(),
  }),
)

// After
import { defineQueryTransport, query } from "@beignet/core/contracts";

listTodos.query(
  z.object({
    limit: z.number().int().optional(),
    tags: z.array(z.string()).optional(),
  }),
  defineQueryTransport({
    limit: query.integer(),
    tags: query.array(query.string()),
  }),
)

Map logical strings, numbers, integers, booleans, RFC 3339 strings, and JavaScript dates with the matching scalar helper. Scalar arrays use repeated query parameters, and query.deepObject(...) supports one flat object. Remove schema coercion that existed only to parse URL strings; the server decodes the declared transport before it runs the schema.

Empty arrays and objects are omitted by default. Opt into { empty: "preserve" } only when Beignet typed clients must distinguish empty from omitted values. Redesign deeper nested query objects because their wire shape is no longer inferred. beignet doctor --strict reports legacy one-argument query declarations that still need a transport.

Event subscriptions expose lifecycle handles

EventBusPort.subscribe(...) and registerListeners(...) now return an EventSubscription instead of a synchronous disposer. Await initial readiness before accepting work and await asynchronous cleanup during shutdown:

// Before
const unregister = eventBus.subscribe(UserRegistered, handleUserRegistered);
unregister();

// After
const subscription = eventBus.subscribe(
  UserRegistered,
  handleUserRegistered,
);
await subscription.ready;
await subscription.unsubscribe();

Move app listener registration from provider setup() into start(), await registration.ready, and keep the handle for stop(). The current beignet make listener output already uses this lifecycle. A listener registry has one 10-second readiness deadline by default; set readyTimeoutMs explicitly when the process needs another bounded startup policy. Startup failure starts all child cleanup within the same deadline; if transport cleanup cannot settle in time, the readiness rejection includes a ListenerRegistrationCleanupTimeoutError instead of blocking indefinitely.

Provider registration uses factories

Better Auth provider options are now explicit, and the package also exposes a direct port adapter for custom wiring:

// Before
createBetterAuthProvider(auth)

// After
createBetterAuthProvider({ auth })

// Direct adapter for app-local provider wiring
createBetterAuthPort({ auth })

Provider packages no longer export shared xProvider instances. Import and call the matching factory in server/providers.ts:

import { createRedisCacheProvider } from "@beignet/provider-cache-redis";

export const providers = [createRedisCacheProvider()] as const;

The same mechanical change applies across first-party providers, for example pinoLoggerProvider becomes createPinoLoggerProvider() and stripePaymentsProvider becomes createStripePaymentsProvider().

Provider factories expose stable named types

First-party provider factories now return named provider types such as RedisCacheProvider, PinoLoggerProvider, and DrizzlePostgresProvider<TSchema>. Their concrete Zod config schemas are private implementation details; exported config interfaces still describe the validated values when an app needs that shape. This does not change provider registration or InferProviderPorts results.

PostgresConfigSchema and MysqlConfigSchema are no longer exported from the Drizzle backend subpaths. Apps that imported either schema should own any app-level environment validation and use PostgresConfig or MysqlConfig for the corresponding validated shape.

Framework-neutral server imports come from core

Route declarations, route registries, hooks, and framework-neutral server types come from @beignet/core/server. Runtime adapter packages expose only their platform-specific APIs:

import {
  contractsFromRoutes,
  createRoutes,
  defineRoutes,
} from "@beignet/core/server";
import { createNextServer, createNextServerLoader } from "@beignet/next";
import type { AppContext } from "@/app-context";

const { defineRouteGroup } = createRoutes<AppContext>();

Use @beignet/web for Web Fetch conversion and server adapters. In particular, replace toNextResponse(...) with toWebResponse(...) imported from @beignet/web.

Test helpers use one subpath

Move imports from @beignet/core/ports/testing to @beignet/core/testing. Fixtures, recording adapters, actors, policy helpers, assertions, factories, seeds, and provider test installation now share that single public boundary.

Schedule instrumentation uses shared provider targets

The schedule-specific ScheduleInstrumentation type was removed. createInlineScheduleRunner(...) now accepts a shared provider instrumentation target; existing record-only sinks remain structurally compatible. You can also pass the complete context ports object when running a schedule inside an application boundary:

const runner = createInlineScheduleRunner({
  ctx,
  instrumentation: ctx.ports,
});

The runner now follows the shared provider instrumentation contract, records events under the schedules watcher, and isolates instrumentation sink failures. onHookError now reports lifecycle hook failures only.

Route declarations use an app-bound builder

Create the route builders once and import them from feature route files:

// lib/routes.ts
import "@beignet/core/server-only";
import { createRoutes } from "@beignet/core/server";
import type { AppContext } from "@/app-context";

export const { defineRoute, defineRouteGroup } = createRoutes<AppContext>();

Replace defineRoute<AppContext>()(...) and defineRouteGroup<AppContext>()(...) calls with the corresponding app-bound builder. Add paths.routesBuilder when the file is not lib/routes.ts.

Listener identity is the first argument

Move listener names out of the options object and put the event in options:

defineListener("posts.enqueue-published-email", {
  event: PostPublished,
  async handle({ payload, ctx }) {},
});

Integration names match their role

The webhook packages are route-bound server integrations. They no longer carry provider audit metadata or appear in beignet provider audit.

Removed aliases

RemovedReplacement
contract.pathTemplatecontract.path
defineFlagRegistry(...)defineFlags(...)
ctx.ports.db.dbctx.ports.db.drizzle
defineRoute<AppContext>()(...)app-bound defineRoute(...) from lib/routes.ts
defineRouteGroup<AppContext>()(...)app-bound defineRouteGroup(...) from lib/routes.ts
createAuthBetterAuthProvider(...)createBetterAuthProvider(...)
@beignet/provider-webhooks-github@beignet/webhooks-github
@beignet/provider-webhooks-stripe@beignet/webhooks-stripe
toNextResponse(...)toWebResponse(...) from @beignet/web
createInMemoryEventBus(...)createMemoryEventBus(...)
createInMemoryEventBusProvider(...)createMemoryEventBusProvider(...)
InMemoryEventBus* typesMemoryEventBus* types
ScheduleInstrumentationProviderInstrumentationTarget from @beignet/core/providers

Getting help from the compiler

Most Beignet migrations are import or option-shape changes. TypeScript should identify every affected call site. When a removed symbol appears only in a generated registry or configuration string, beignet doctor --strict and beignet provider audit cover the registration and provider metadata that the compiler cannot inspect.

See Stability and releases for the compatibility policy.