Buoy for Angular
Let your coding agent install Buoy. Copy the Angular install prompt. Its guide is install-angular.md. Watch the trailer on the Angular page.
Buoy runs in Angular as a beta. It starts after the first view, outside Angular's zone. It stops with the app and skips the server. Your app keeps its Angular views and state. Buoy uses React for its own UI.
Install and sign in
Run these from your app folder:
ng add @buoy-gg/angular
npx --package=@buoy-gg/core buoy login
ng serve
Pick the tools you want in the prompt. Network, Console, Storage, Events, and Env start checked. The prompt also checks tools that fit the app:
- Routes if the app has Angular Router.
- Query if it has Angular Query.
- Redux if it has NgRx or NGXS.
- Highlight on Angular 20 or newer.
React, React DOM, and RN Web come with Buoy.
Use the same version for all Buoy packages.
To add a tool later, install its Buoy package.
Restart ng serve to load the new list.
The server prints a restart note when that list changes.
A second ng add run leaves your files alone.
For more than one app, choose it by name:
ng add @buoy-gg/angular --project my-app
npx --package=@buoy-gg/core buoy login --project my-app
Login writes src/environments/buoy.ts under the app's source root.
The builder reads its BUOY_KEY for you.
Login also writes an env file for local Buoy tools.
It adds both files to .gitignore. Do not commit them.
Dev tokens and free keys keep a dev guard in the key file.
Each person signs in with their own account.
What ng add writes
In angular.json, it changes two builder names:
{
"build": {
"builder": "@buoy-gg/angular:application"
},
"serve": {
"builder": "@buoy-gg/angular:dev-server"
}
}
Keep each target's other fields and build configs.
ng add keeps those fields when it makes the change.
It adds this call last in app.config.ts:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideBuoy } from '@buoy-gg/angular';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Keep the app's other providers here.
provideBuoy(),
],
};
For NgModule apps, it puts the call last in @NgModule.providers.
The builder adds the light hook before the app starts.
That lets Network see calls from the first app initializer.
It loads all installed tools. You need no buoy.ts list.
You need no Buoy guard or prod condition here.
Prod swaps Buoy for a small, empty provider. The host, tools, React DOM, and RN Web stay out. NgRx DevTools needs its own dev guard, shown below. SSR and prerender builds keep Buoy out of server code. Watch builds use the same rules.
QA builds
To keep Buoy in a QA build, set this build option:
{
"buoy": { "release": true }
}
Put it in the QA config under build.configurations.
It keeps the browser tools and turns on the early hook.
The server bundle still uses the empty provider.
For release access, your team signs in with Buoy.
Add your site at buoy.gg/dashboard/sites.
Then pass provideBuoy({ signIn: true }).
A paid key works too, as a backup.
Your app's access rules and plan still apply.
Dev tokens and free keys from login stay out of prod code.
NgRx, NGXS, and Highlight still need Angular dev mode.
NgRx Store
Keep your normal store and effects setup.
Add @ngrx/store-devtools and select the Redux tool.
Keep NgRx DevTools in a dev guard.
Put provideBuoy() last in the provider list:
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideBuoy } from '@buoy-gg/angular';
declare const ngDevMode: boolean | undefined;
// At the end of the app's providers:
(typeof ngDevMode === 'undefined' || ngDevMode)
? provideStoreDevtools({ maxAge: 50 })
: [],
provideBuoy(),
No extra NgRx bridge call is needed with the builder.
Buoy provides REDUX_DEVTOOLS_EXTENSION through DI.
It never claims window.__REDUX_DEVTOOLS_EXTENSION__.
If another provider wins, dev builds print this warning:
[Buoy] NgRx has no Buoy link. Move provideBuoy() last in providers.
The tool shows actions, state, and diffs.
Replay adds a new row. Jump restores the chosen state.
A state jump may take one second.
The guard keeps NgRx DevTools out of prod.
logOnly alone still ships that code.
Old setups can keep provideBuoyNgRx() after provideStoreDevtools().
NGXS
Select the Redux tool and keep the app's NGXS store. NGXS still needs its own adapter call:
import { provideBuoy } from '@buoy-gg/angular';
import { provideBuoyNgxs } from '@buoy-gg/angular/ngxs';
declare const ngDevMode: boolean | undefined;
// At the end of the app's providers:
(typeof ngDevMode === 'undefined' || ngDevMode)
? provideBuoyNgxs()
: [],
provideBuoy(),
This uses NGXS_PLUGINS through DI.
It needs no browser global or @ngxs/devtools-plugin.
We tested NGXS 22 with RxJS 7.8.
Use one adapter. Do not mix NgRx and NGXS bridges.
Jump calls Store.reset. Replay calls Store.dispatch.
Async actions show when they end. Errors still reach NGXS.
Canceled actions with no result add no row.
Router, Query, and Highlight
With the builder, provideBuoy() links these tools through DI.
Routes reads the app's Router and route list.
Go asks for id in a path like /pokemon/:id.
Lazy child routes show once the app loads them.
Query uses the app's Angular Query client.
Keep your normal provideTanStackQuery(queryClient) call.
When you choose Query, ng add adds @tanstack/react-query.
Buoy's UI needs that peer next to @buoy-gg/react-query.
Match its version to @tanstack/angular-query-experimental.
Both must resolve the same @tanstack/query-core version and copy.
Check with pnpm why @tanstack/query-core or npm ls @tanstack/query-core.
Split copies can break types or the offline switch.
Keys, data, and refetch all use the app's client.
Highlight draws boxes and counts checks for each view. It needs Angular 20 or newer and the dev hook. Missing hooks show "Not in this app." Buoy leaves other hooks in place and stops its own. Counts are view checks, not paints or DOM writes. The list holds up to 200 views. It does not give React's cause or timing data. The hook is private API. Check it on Angular upgrades.
Angular 19 and custom builders
The Buoy builders work with CLI 19, 20, 21, and 22. Fresh app runs cover 19, 20, and 22. CLI 21 was checked from its types and schema. Highlight stays off on Angular 19.
CLI 19 has no conditions option.
The Buoy builder does its own prod swap on that CLI.
A fresh CLI 19 app may use @angular-devkit/build-angular.
ng add keeps that builder and writes the manual setup.
The same rule applies to custom builders and Nx targets.
ng add reads the targets in angular.json.
It only swaps @angular/build builder names.
For other builders, it prints why it kept them.
It writes the hook, tool list, dev guard, and prod config.
CLI 20+ gets the buoy-disabled condition.
Check that your custom builder accepts conditions.
If it does not, use entry file swaps instead.
CLI 19 and webpack get entry file swaps instead.
The tool list gets a prod file swap too.
Keep manual app links for Router, Query, and state tools.
For Nx layouts with no angular.json, use manual setup.
Other stores and phone apps
Vanilla Zustand stores need named store objects.
Pass zustandStores: { ui: uiStore } in app options.
Install the Zustand tool. The builder finds it.
It shows state, changes, diffs, and edits.
SignalStore has no Buoy DI feature yet. Do not use the NgRx bridge for SignalStore. A Signals view is planned. It has not shipped. It would show signals used by the current page. It cannot list every signal in an app. Jotai needs an app with Jotai state.
Angular 22 uses fetch for HttpClient by default.
Use provideHttpClient(withXhr()) if the app needs XHR.
For Ionic Angular phones, see the Capacitor guide.
Desktop links and phone boot hooks still need their own setup.
App options
Options add to the setup the builder finds.
Keep the Buoy call last in the provider list.
Pass an object, or a factory that calls inject():
provideBuoy({ zustandStores: { ui: uiStore } }),
// Or use a factory for app services:
provideBuoy(() => ({ queryClient: inject(QueryClient) })),
Choose one call. Do not use both.
The factory runs once in the app's DI context.
It can read the store with inject(Store) too.
Router, Query, Highlight, and NgRx still get their links.
The builder still loads each installed tool.
Custom apps and a test broker work here too.
The merge rules are:
- App keys replace auto keys. Your
queryClientwins. load()adds its keys last, after the app options.- Lists and nested objects replace the whole value.
This includes
apps,zustandStores, andexternalSync. linksmerges by tool id. Your link wins for that id. Each link runs after render and returns a stop function. Buoy calls each stop on teardown, plus youronDestroy.- An explicit
modulesmap or loader turns auto off. It keeps the manual path below, with your own links.
To use your own Routes link, pass this in the options:
links: {
'route-events': async () => {
const { connectBuoyRouter } = await import('@buoy-gg/angular/router');
return zone.runOutsideAngular(() =>
connectBuoyRouter(router, fn => zone.run(fn)),
);
},
},
Get router and zone with inject() in the factory.
This replaces just the Routes link. Highlight stays on.
Use highlight-updates to replace that link too.
For lazy app settings, let load() return mount options.
Leave out modules to keep the builder's tool list.
The loader and link callbacks run outside the zone.
They run later, so call inject() in the factory.
Manual setup
Use this path when you pass your own tool list.
Existing provideBuoy({ modules, ... }) calls still work.
A factory, provideBuoy(() => options), can call inject().
load can return all mount settings at once.
onDestroy stops app hooks; onMount gets the stop function.
An explicit modules map or loader selects this path.
With the builder, other options keep the auto links.
Put the light hook first in src/main.ts:
import '@buoy-gg/angular/register';
Do not use the React inspector hook from @buoy-gg/core/web/register.
Make a tool list, then pass it behind a dev guard:
// src/buoy.ts
import { BUOY_KEY } from './environments/buoy';
export const buoyOptions = {
licenseKey: BUOY_KEY,
modules: async () => ({
network: await import('@buoy-gg/network/web'),
}),
};
// src/app/app.config.ts
import { provideBuoy } from '@buoy-gg/angular';
import { buoyOptions } from '../buoy';
declare const ngDevMode: boolean | undefined;
// Last in the app's providers:
(typeof ngDevMode === 'undefined' || ngDevMode)
? provideBuoy(buoyOptions)
: [],
For CLI 20+ esbuild builders that accept conditions, use:
{
"conditions": ["buoy-disabled", "module", "production"],
"fileReplacements": [
{ "replace": "src/buoy.ts", "with": "src/buoy.prod.ts" }
]
}
// src/buoy.prod.ts
export const buoyOptions = {};
Keep both the guard and the file swap.
A runtime isDevMode() check alone leaves tool chunks.
For CLI 19 or webpack, let ng add write entry swaps.
Or use prod entry and config files with no Buoy imports.
Keep the app's own providers in those prod files.
In a manual loader, use these app links as needed:
| Tool | Manual link |
|---|---|
| Routes | Call connectBuoyRouter(router, fn => zone.run(fn)) from @buoy-gg/angular/router. Stop it in onDestroy. |
| Highlight | Call connectBuoyHighlights(module.HighlightUpdatesController, fn => zone.runOutsideAngular(fn)) from @buoy-gg/angular/highlights. Stop it in onDestroy. |
| NgRx | Put provideBuoyNgRx() after NgRx DevTools, inside the same dev guard. |
| Query | Pass the app's queryClient and the 'react-query' module. |
| Zustand | Pass zustandStores and the zustand module. |
Manual QA builds also need enableInRelease: true.
Keep their app guard true and omit the prod swaps.
Start registerBrowserNetwork({ enableInRelease: true }) from the light hook entry.
A Desktop link needs externalSync: { enableInRelease: true } too.
Keep that flag in the settings returned by load.
Remote phone checks may need these calls before tools open:
bootLifecycle(), installLocationAdapters(), installPermissions(), and
installCapacitorNotificationCapture({ enableInRelease: true }).
Use only the modules installed in that app.