Repository navigation
Zitadel NextGen v0.1.0
0.1.0 is the first proper release of Zitadel NextGen, and the first on npm's latest tag. Registration and login live in your app, under your brand, while Zitadel guards the credentials, sessions and tokens underneath. An empty folder becomes an app with a working login in three commands, with no account and no Docker:
npx @zitadel/cli@latest start
npx @zitadel/cli@latest setup --framework next --server local
npm run devThis release sums up everything the alpha previews built; those versions are deprecated now. NextGen is still a preview: until 1.0.0, a minor release can include breaking changes, and each one is called out as breaking. The quickstart walks you through it.
What you get
- Sign-in and sign-up in your app.
<zitadel-login>runs the whole flow in one element: email and password, passkeys, and social sign-in with Google or any OIDC provider, where the first sign-in creates the account.<zitadel-session>and<zitadel-logout>show who is signed in and sign them out. For a UI of your own, the flow and auth-attempt APIs drive sign-in headless. Components - 12 frameworks.
zitadel setupcreates a new app or patches an existing one with login, registration and profile pages, for Next.js, Nuxt, SvelteKit, SolidStart, Qwik Router, TanStack Start, React, Vue, Angular, Solid, Svelte and Qwik. The server SDKs keep auth on your own origin: they check sessions, protect routes and keep the project secret on the server. SDKs - Users shaped by your schema. A user schema in JSON Schema says which attributes your users have, which one they sign in with, which must be unique and which sign-in methods they may use. Every write is checked against it. Flows and schemas
- Auth as code. Schemas, login flows, branding and sign-in connections are files in your repository.
zitadel planchecks them against the server's rules before anything changes, andzitadel applypublishes them as immutable revisions. Plan and apply - Your brand, your words. Theme the login with
--zl-*tokens and light and dark branding, or take over its markup withzitadel branding eject. Override any text per language; English, German and Italian are built in. Design, copy - A console. At
/ui/console/, manage users through forms built from your schema, as well as teams and project admins, and look through your schemas, flows, sign-in methods and branding, with a live preview of the login. - Create first, claim later.
zitadel setup --server https://preview.zitadel.comcreates a hosted project without an account, andzitadel claimattaches it to your team within 14 days. Projects and teams - Runs anywhere.
zitadel startruns the server from npm on your machine, with no Docker, Go or checkout, and signs you in to the console with a one-time link. To host it, take the binary for macOS, Linux or Windows or the imageghcr.io/zitadel/nextgen, with SQLite by default and PostgreSQL or Spanner in production. Local runtime, databases - Audit events. Sign-ins, sessions, users and configuration changes are recorded as typed events you can query through the API, and a self-hosted server can export them to a webhook. Events
- One API for all of it. Every operation is in the API reference, with stable error codes.
zitadel apicalls any of them from the terminal, and@zitadel/apiis a typed client with an error type per operation. API client - Built for agents. Every CLI command answers
--jsonwith one envelope.npx skills add zitadel/nextgen --full-depthinstalls Agent Skills for the CLI, the components and each SDK, and the docs come asllms.txtand over MCP. Agents - Tests against a real server.
@zitadel/testingstarts a local Zitadel for Playwright, with seeded users, signed-in pages and virtual passkeys.
Not there yet
- Password recovery, email or SMS codes, TOTP, email verification and enforced MFA. The default flow covers sign-in and sign-up with passwords, passkeys and social sign-in.
- Social sign-in is OIDC only, and a provider can't be linked to an existing account yet.
- Zitadel doesn't issue OIDC or OAuth tokens to your apps in this preview: apps use its sessions through the SDKs, and the SPA frameworks need a proxy of your own in production.
- Deploying a release to your own domains goes through the API; deploying to preview URLs, rollback and a CLI command for it are still to come.
Upgrading from 1.0.0-alpha.N
- Install with
@latest, for the CLI and every SDK;@alphanow points at0.1.0and stays there. npm sorts0.1.0below1.0.0-alpha.28, so Renovate and Dependabot won't offer this update: set the@zitadel/*versions in yourpackage.jsonto0.1.0yourself. The GitHub releasesv1.0.0-alpha.20to.28are nowv0.1.0-alpha.20to.28. - Security: an auth attempt could issue a session after verifying only the user's identifier (#2211). Upgrade the server, then revoke active sessions whose only factor is
identifier. Step-up sign-in now needs the session'ssession_tokenas well as itssession_id(#2166). - Breaking: a deploy that names a primary origin answers
400withdep.invalid; deploy withtargets: ["primary"](#1736). Also check the new error codes for schema failures (req.invalid, #1894) and over-long passwords (user.password_policy_violation, #2107), and howPOST /flowpicks the release byOrigin(#2043).
The changes listed below are the ones since 1.0.0-alpha.28, the last alpha.
Generated Release Facts
This section is maintained by release automation. Edit product notes outside this block.
Release commit: c621117bceb2
Container Images
| Image | Reference |
|---|---|
| container | ghcr.io/zitadel/nextgen:0.1.0 |
| container | ghcr.io/zitadel/nextgen:latest |
npm Packages
| Package | Version |
|---|---|
@zitadel/cli |
0.1.0 |
@zitadel/server |
0.1.0 |
@zitadel/server-linux-x64 |
0.1.0 |
@zitadel/server-linux-arm64 |
0.1.0 |
@zitadel/server-darwin-x64 |
0.1.0 |
@zitadel/server-darwin-arm64 |
0.1.0 |
@zitadel/server-win32-x64 |
0.1.0 |
@zitadel/api |
0.1.0 |
@zitadel/config |
0.1.0 |
@zitadel/components |
0.1.0 |
@zitadel/sdk-core |
0.1.0 |
@zitadel/sdk-next |
0.1.0 |
@zitadel/sdk-nuxt |
0.1.0 |
@zitadel/sdk-react |
0.1.0 |
@zitadel/sdk-vue |
0.1.0 |
@zitadel/sdk-angular |
0.1.0 |
@zitadel/sdk-solid |
0.1.0 |
@zitadel/sdk-svelte |
0.1.0 |
@zitadel/sdk-qwik |
0.1.0 |
@zitadel/sdk-sveltekit |
0.1.0 |
@zitadel/sdk-solid-start |
0.1.0 |
@zitadel/sdk-qwik-router |
0.1.0 |
@zitadel/sdk-tanstack-start |
0.1.0 |
@zitadel/testing |
0.1.0 |
Package Changes
@zitadel/cli
Minor Changes
-
#2187
114c32cThanks @fforootd! - Addzitadel api, which calls any operation of the Zitadel API with your project's server and secret, including the ones no command covers yet. Name an operation by itsoperationId(zitadel api queryUsers) or its path (zitadel api /users/query), set query parameters and body fields with-F name=value, typed by the operation's schema, pass a whole body with--input, follow every page with--paginate, and preview the request as cURL with--dry-run.zitadel api lslists the operations, and--jsonreturns the response body asdata.@zitadel/apiexports the operations as data from@zitadel/api/generated/operations: each one's method, path, parameters, authentication and paging. -
#2215
ab19e1eThanks @fforootd! - Releases now publish to npm'slatesttag as0.xpreview versions, starting at0.1.0, instead of1.0.0-alpha.Nunderalpha: run the CLI asnpx @zitadel/cli@latestand install the SDKs with@latest. The1.0.0-alpha.Nversions are deprecated on npm. Until 1.0.0, a breaking change ships in a minor version, and its release note marks it Breaking:.zitadel setuppins the@zitadel/*packages it adds, and the commands it suggests, to the CLI's exact version on every release, andzitadel start --runtime dockerruns the server image of that version.
Patch Changes
-
#2190
43b20eaThanks @mridang! -zitadel setupnow patches an existing SvelteKit, SolidStart, Qwik City or TanStack Start app instead of failing on it. The four patchers wrote the app shell and landing page (SvelteKit's+layout.svelteand+page.svelte, the other three'sroutes/index.tsx) on every run, so setup stopped withE_CONFLICTon any app that already had them and--forceoverwrote the user's own root layout and homepage. Like the Next and Nuxt patchers, they now write those files only when setup created the app skeleton itself and report them as conditionally scaffolded, so a pre-existing app keeps its shell and landing page while still getting the auth boundary and the login, register and profile routes. -
#2177
9194819Thanks @mridang! - Add@zitadel/sdk-next/components, a"use client"entry that re-exports the ReactZitadelLogin,ZitadelLogoutandZitadelSessionwrappers, soprojectandlocalesreach the element as properties after server-rendered hydration on React 19 and on React 18, where the raw elements lose them or write[object Object]attributes. The Next.js pageszitadel setupscaffolds now render the wrappers.
@zitadel/server
Minor Changes
-
#2142
f4115f8Thanks @IAM-marco! - The console's user detail screen now edits a boolean attribute with a switch and saves it astrueorfalse. Optional fields are marked "Optional" beside their label. A cleared required field now says "This field is required." next to it, and a partly cleared date keeps Save off instead of deleting the stored date. An unsaved edit now survives a switch to the Authentication tab and back. The fields are locked while a save runs, so nothing typed meanwhile is lost. -
#2135
7066bf8Thanks @IAM-marco! - The console's user detail screen now shows which schema revision a user is on, and links to that schema. -
#1884
8ef9c49Thanks @vitorbari! -GET /deployments/{deployment_id}reads one deployment andGET /deploymentslists them newest first, each with itskindandstatus.originnarrows the list to one origin,live=truelists what is served now, andexpand=releaseembeds the release each deployment made live. -
#1736
77a4f86Thanks @vitorbari! - Every deployment carries akind,primaryorpreview, and astatus,liveorinactive. Primary origins are deployed as a whole withtargets: ["primary"], so they all serve the same deployment, and a rollback re-deploys to every primary origin the project has.Breaking: a deploy that names a primary origin answers
400withdep.invalid; deploy toprimaryinstead. -
#1883
dedd93bThanks @vitorbari! - Releases can now be deployed to a project's primary origins withPOST /deploymentsandtargets: ["primary"], which covers every primary origin. Deploying the release that is already live answers200and writes nothing, andexpected_deployment_idguards against racing another deploy with409dep.conflict. -
#1886
1653960Thanks @peintnermax! -GET /users/{user_id}/passwordreports when the user's current password was set (set_at), and leaves it out when the user has none. It never returns the password or its hash. It takesuser.read, or a Console session with access to the user's project, and the generated client gainsgetUserPasswordState. -
#2107
f80ea64Thanks @livio-a! - A password longer than 64 characters is now rejected with the error codeuser.password_policy_violationinstead ofuser.password_too_longwhen it is set through the API (PUT /users/{user_id}/password); the response'sdetails.detailsholds{"violations": {"max_length": 64}}. During sign-up, with a password or through a social provider, the same rejection now appears as an error on the sign-up step instead of the request failing; in the default sign-up flow that is the step with the password field, so the user can choose another password. Clients that match onuser.password_too_longfor over-long passwords should also handleuser.password_policy_violation;user.password_too_longremains for passwords the project's hashing method cannot take. -
#2043
0e1ee74Thanks @adlerhurst! - Runtime operations document how a request finds its release from the browserOriginheader.POST /flowtakesOriginas the input that selects the deployment; an origin that matches no project pattern answers403withproj.origin_not_allowed, a preview URL with no live preview answers403withproj.preview_not_live, nothing deployed answers409withrel.not_deployed, and a revoked release answers409withrel.revoked. Later flow steps and app-plane auth-attempt operations re-use the sealed deployment and only refuse withrel.revoked(plusproj.mismatch/rel.not_deployedon create and handoff exchange). The server still serves today's behavior until the resolution path lands. -
5dea188Thanks @adlerhurst! -POST /flowresolves the release from the browserOriginheader: the gate admits the origin against the project's patterns and live previews, the newest deployment to that exact origin is the target, and that deployment's release is sealed into_zflowasdeployment_idandrelease_id. An origin that matches no pattern answers403withproj.origin_not_allowed, a preview URL with no live preview answers403withproj.preview_not_live, nothing deployed to a pattern-matched origin answers409withrel.not_deployed, and a revoked release answers409withrel.revoked. A request with noOrigin, or an open admission (empty allowlist / loopback) with nothing deployed yet, keeps the previous newest-revision path until the project default target lands. -
#2160
4d661a6Thanks @fforootd! - The "You already have an account" step after a sign-in with an identity provider now carries the account's email when the provider verified it (asstep.identifier, so a password manager offers the matching password), no longer offers the provider that just led there, and offers "Use a passkey" only to an account that has one. -
#1894
aae6e07Thanks @wim07101993! - User attribute errors now name the fields to fix. WhenPOST /users,PATCH /users/{user_id}orPATCH /users/merefuses attributes that fail the user schema, the answer is400 req.invalid, the same as for a malformed body, anddetails.details.fieldslists every failing field by its path from the request body, such asattributes.countryorattributes.address.zip. A unique value another user holds still answers409 user.already_exists, anddetails.details.fieldsnow names the attributes whose value is taken in the same form, without naming the user who holds it. Clients that matcheduser.invalidfor schema failures should matchreq.invalid. -
#2202
831cb81Thanks @peintnermax! - AddGET /users/me/passkeys,GET /users/me/identity_linksandGET /users/me/password: the signed-in user's own passkeys, linked identity provider accounts and password state, readable with the session alone. Like the other/users/mereads they take an optionalproject_id, and refuse a session of another project with403 sess.project_mismatch. The@zitadel/apiclient gainslistMyPasskeys(),listMyIdentityLinks()andgetMyPasswordState().
Patch Changes
-
#2211
d66f2eeThanks @fforootd! - Fix an authentication bypass: a sign-in throughPOST /auth_attemptscould
issue a session after verifying only the user's identifier, with no password,
passkey, or SSO. An attempt created without required checks counted as complete
as soon as the identifier resolved, and the project's publishable key, which is
public by design, can call these endpoints, so anyone holding it could sign in
as any user whose identifier they knew.An attempt now completes only once it carries a verified password, passkey, or
SSO factor, andPOST /auth_attempts/{id}/handoffrefuses one without it with
att.not_completed. A client that drives the auth-attempt API directly must
verify one of those factors before it asks for a handoff. Sessions issued
through this gap before you upgrade stay valid until they expire: list your
active sessions and revoke any whose only factor isidentifier. -
#1885
b69348eThanks @peintnermax! - The console's user detail screen follows the updated design. Overview heads the
profile with the user's schema and saves edited attributes with a Save button;
only the changed attributes are sent, and a cleared optional field is removed.
The Authentication tab lists the passkey state as a label and its value, with
when the passkey was verified, and the identity providers in the same style. -
#2192
54000c1Thanks @peintnermax! - The console's users list keeps its attribute columns in the same order on every load; before, a list mixing user schemas could reshuffle them depending on which schema loaded first. Each row's ID gains a copy button, shown on hover or focus, which copies the whole ID even where the column truncates it. -
#2188
17523dcThanks @fforootd! - Declare the error codes that the branding, event, team, project-claim and health operations return. Their error response described only the generic{ code, message }shape, so the OpenAPI document and the API reference did not list which codes these 17 operations can send. Each now names its codes, like every other operation, and the@zitadel/apiclient gains a code-discriminated<Operation>Defaulttype for each (for exampleGetTeamDefault) to branch on. -
#2166
fe8b825Thanks @fforootd! - Step-up sign-in now requires proof that the caller holds the session.POST /auth_attemptsandPOST /flowtake a newsession_tokenfield. A request that names an existingsession_idmust also send that session'ssession_token, asPOST /sessionsorPOST /sessions/exchangereturned it. A request that sends onlysession_tokenruns against the token's session. Before this, asession_idalone was accepted. The new attempt then inherited that session's verified factors, so a caller who knew a session id but did not hold the session could complete a step-up on it and receive a fresh session token. Asession_idwithout a matchingsession_tokenof the same project is now refused with400(att.invalid_requeston attempts,req.invalidon flows).
@zitadel/api
Minor Changes
-
#2187
114c32cThanks @fforootd! - Addzitadel api, which calls any operation of the Zitadel API with your project's server and secret, including the ones no command covers yet. Name an operation by itsoperationId(zitadel api queryUsers) or its path (zitadel api /users/query), set query parameters and body fields with-F name=value, typed by the operation's schema, pass a whole body with--input, follow every page with--paginate, and preview the request as cURL with--dry-run.zitadel api lslists the operations, and--jsonreturns the response body asdata.@zitadel/apiexports the operations as data from@zitadel/api/generated/operations: each one's method, path, parameters, authentication and paging. -
#1884
8ef9c49Thanks @vitorbari! -GET /deployments/{deployment_id}reads one deployment andGET /deploymentslists them newest first, each with itskindandstatus.originnarrows the list to one origin,live=truelists what is served now, andexpand=releaseembeds the release each deployment made live. -
#1883
dedd93bThanks @vitorbari! - Releases can now be deployed to a project's primary origins withPOST /deploymentsandtargets: ["primary"], which covers every primary origin. Deploying the release that is already live answers200and writes nothing, andexpected_deployment_idguards against racing another deploy with409dep.conflict. -
#1886
1653960Thanks @peintnermax! -GET /users/{user_id}/passwordreports when the user's current password was set (set_at), and leaves it out when the user has none. It never returns the password or its hash. It takesuser.read, or a Console session with access to the user's project, and the generated client gainsgetUserPasswordState. -
#2202
831cb81Thanks @peintnermax! - AddGET /users/me/passkeys,GET /users/me/identity_linksandGET /users/me/password: the signed-in user's own passkeys, linked identity provider accounts and password state, readable with the session alone. Like the other/users/mereads they take an optionalproject_id, and refuse a session of another project with403 sess.project_mismatch. The@zitadel/apiclient gainslistMyPasskeys(),listMyIdentityLinks()andgetMyPasswordState().
Patch Changes
- #2188
17523dcThanks @fforootd! - Declare the error codes that the branding, event, team, project-claim and health operations return. Their error response described only the generic{ code, message }shape, so the OpenAPI document and the API reference did not list which codes these 17 operations can send. Each now names its codes, like every other operation, and the@zitadel/apiclient gains a code-discriminated<Operation>Defaulttype for each (for exampleGetTeamDefault) to branch on.
@zitadel/config
Patch Changes
-
#2155
b44d6f9Thanks @fforootd! - The password step of<zitadel-login>now shows the account it signs in to, with a "Use another account" link that goes back to the step where the email was typed. Templates ejected withzitadel branding ejectbefore this release keep their markup; eject again to get the account row. -
#2155
b44d6f9Thanks @fforootd! -<zitadel-login>shows a spinner only on the button that was used: following the "Sign up" or "Sign in" link no longer spins the "Continue" button, and a secondary button such as "Use a passkey" now shows its own progress.
@zitadel/components
Minor Changes
- #2155
b44d6f9Thanks @fforootd! - The password step of<zitadel-login>now shows the account it signs in to, with a "Use another account" link that goes back to the step where the email was typed. Templates ejected withzitadel branding ejectbefore this release keep their markup; eject again to get the account row.
Patch Changes
-
#2199
5680eb7Thanks @mridang! - Generate and publish acustom-elements.jsonmanifest describing every
zitadel-*andzl-*element's attributes and events, exposed through the
package'scustomElementsfield so editors and framework tooling can discover
the elements' surface. -
#2090
e3cd8f8Thanks @mridang! -<zitadel-login>no longer dispatcheszitadel-flow-inputfor a password or one-time-code field, so wiringonFlowInputto logging or analytics cannot capture what a visitor types there. Other fields dispatch as before. The atoms' internalzl-input,zl-changeandzl-passkey-resultevents are no longer composed, so they stay inside the widget. -
#2175
be96308Thanks @fforootd! -<zitadel-login>now falls back to yourenentries inlocalesfor any text
the visitor's language has none for. A step you labelled only in English reads
in English for every visitor instead of showing raw keys, and visitors in a
language without built-in texts, such as French, see the English texts you
changed. Built-in German and Italian translations still win over your English;
adddeoritentries to change those. -
#2155
b44d6f9Thanks @fforootd! -<zitadel-login>shows a spinner only on the button that was used: following the "Sign up" or "Sign in" link no longer spins the "Continue" button, and a secondary button such as "Use a passkey" now shows its own progress. -
#2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-core
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-next
Patch Changes
- #2177
9194819Thanks @mridang! - Add@zitadel/sdk-next/components, a"use client"entry that re-exports the ReactZitadelLogin,ZitadelLogoutandZitadelSessionwrappers, soprojectandlocalesreach the element as properties after server-rendered hydration on React 19 and on React 18, where the raw elements lose them or write[object Object]attributes. The Next.js pageszitadel setupscaffolds now render the wrappers.
@zitadel/sdk-react
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-vue
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-angular
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-solid
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-svelte
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.
@zitadel/sdk-qwik
Patch Changes
- #2178
3b206b2Thanks @mridang! - The widgets now wait for their listeners before they navigate. The detail ofzitadel-flow-redirect,zitadel-flow-completeandzitadel-signoutcarries awaitUntil(promise)function; the widget navigates once every promise a listener passed has settled, or after two seconds, and at once when none was passed. The React, Solid, Svelte and Qwik wrappers forward each callback's returned promise towaitUntil, so an async or lazily loaded handler runs before the page unloads; the Vue and Angular wrappers exposewaitUntilon the event detail for a handler to call itself.