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
- Read the target release notes and the migration section on this page.
- Update all Beignet packages to the same version and reinstall dependencies.
- Run
bun beignet doctor --strictandbun beignet lint. - Run the app's
typecheck,test, and productionbuildscripts. - 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
- Replace
createAuthBetterAuthProvider(...)withcreateBetterAuthProvider(...). - Replace
@beignet/provider-webhooks-githubwith@beignet/webhooks-github. - Replace
@beignet/provider-webhooks-stripewith@beignet/webhooks-stripe.
The webhook packages are route-bound server integrations. They no longer carry
provider audit metadata or appear in beignet provider audit.
Removed aliases
| Removed | Replacement |
|---|---|
contract.pathTemplate | contract.path |
defineFlagRegistry(...) | defineFlags(...) |
ctx.ports.db.db | ctx.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* types | MemoryEventBus* types |
ScheduleInstrumentation | ProviderInstrumentationTarget 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.