Jazz 0.10.0 is out!

For Jazz 0.10.0 we have been focusing on enhancing authentication to make it optional, more flexible and easier to use.

The default is now anonymous auth, which means that you can build the functionality of your app first and figure out auth later. For users this means that they can start using your app right away on one device -- and once you integrate an auth method, users can sign up and their anonymous accounts are transparently upgraded to authenticated accounts that work across devices.

There are also some other minor improvements that will make your Jazz experience even better!

What's new?

Here is what's changed in this release:

  • New authentication flow: Now with anonymous auth, redesigned to make Jazz easier to start with and be more flexible.
  • Local-only mode: Users can now explore your app in local-only mode before signing up.
  • Improvements on the loading APIs; ensureLoaded now always returns a value and useCoState now returns null if the value is not found.
  • Jazz Workers on native WebSockets: Improves compatibility with a wider set of Javascript runtimes.
  • Group inheritance with role mapping: Groups can now inherit members from other groups with a fixed role.
  • Support for Node 14 dropped on cojson.
  • Bugfix: Group.removeMember now returns a promise.
  • Now cojson and jazz-tools don't export directly the crypto providers anymore. Replace the import with cojson/crypto/WasmCrypto or cojson/crypto/PureJSCrypto depending on your use case.

New authentication flow

Up until now authentication has been the first part to figure out when building a Jazz app, and this was a stumbling block for many.

Now it is no longer required and setting up a Jazz app is as easy as writing this:

<JazzProvider auth={authMethod} // removed peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} > <App /> </JazzProvider>

New users are initially authenticated as "anonymous" and you can sign them up later with the hooks/providers of your chosen auth method.

Your auth code can now blend into your component tree:

export function AuthModal({ open, onOpenChange }: AuthModalProps) { const [username, setUsername] = useState(""); const [isSignUp, setIsSignUp] = useState(true); const auth = usePasskeyAuth({ // Must be inside the JazzProvider! appName: "My super-cool web app", }); const handleViewChange = () => { setIsSignUp(!isSignUp); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (isSignUp) { await auth.signUp(username); } else { await auth.logIn(); } onOpenChange(false); };

When a user signs up with one of the auth methods we upgrade the current anonymous account to an authenticated account by storing the account's secret keys in the auth method.

To check if the user has registered you can use the new useIsAuthenticated hook:

export function AuthButton() { const isAuthenticated = useIsAuthenticated(); const { logOut } = useAccount(); const [open, setOpen] = useState(false); if (isAuthenticated) { return ( <Button variant="outline" onClick={logOut} > Sign out </Button> ); } return ( <> <Button onClick={() => setOpen(true)}> Sign up </Button> <AuthModal open={open} onOpenChange={setOpen} /> </> ); }

If you do want to require your users to register before using your app, you can do it by moving the auth code outside of your app and conditionally rendering your app as a child -- or apply this pattern for specific parts of the app you'd like to gate behind authentication.

Our provided "Basic UIs" for the different auth methods render their children conditionally by default, so you can either nest them in your app without children, or nest your app or gated parts of your app inside them, depending on the desired behavior.

import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react"; ReactDOM.createRoot(document.getElementById("root")!).render( <JazzProvider sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} > <PasskeyAuthBasicUI appName="My super-cool web app"> <App /> </PasskeyAuthBasicUI> </JazzProvider> );

For the changes related to the specific auth providers see the updated authentication docs.

Local-only mode

If you are ok with data not being persisted on the sync server for anonymous users, you can now set your app to local-only depending on the user's authentication state.

With sync.when set to "signedUp" the app will work in local-only mode when the user is anonymous and unlock the multiplayer/multi-device features and cloud persistence when they sign up:

<JazzProvider sync={{ peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // This makes the app work in local mode when the user is anonymous when: "signedUp", }} > <App /> </JazzProvider>

You can control when Jazz will sync by switching the when config to "always" or "never".

Improvements on the loading APIs

Before 0.10.0 ensureLoaded was returning a nullable value forcing the Typescript code to always include null checks:

export async function updateActiveTrack(track: MusicTrack) { // No need to pass the account const me = await MusicaAccount.getMe().ensureLoaded({ root: {}, }); if (!me) { // Technically can never happen throw new Error("User not found"); } me.root.activeTrack = track; }

Now ensureLoaded always returns a value, making it's usage leaner:

export async function updateActiveTrack(track: MusicTrack) { // No need to pass the account const { root } = await MusicaAccount.getMe().ensureLoaded({ root: {}, }); // Null checks are no more required root.activeTrack = track; }

It will throw if the value is not found, which can happen if the user tries to resolve a value that is not available.

useCoState now returns null if the value is not found, making it now possible to check against not-found values:

const value = useCoState(MusicTrack, id, {}); if (value === null) { return <div>Track not found</div>; }

Jazz Workers on native WebSockets

We have removed the dependency on ws and switched to the native WebSocket API for Jazz Workers.

This improves the compatibility with a wider set of Javascript runtimes adding drop-in support for Deno, Bun, Browsers and Cloudflare Durable Objects.

If you are using a Node.js version lower than 22 you will need to install the ws package and provide the WebSocket constructor:

import { WebSocket } from "ws"; import { startWorker } from "jazz-nodejs"; const { worker } = await startWorker({ WebSocket, });

Group inheritance with role mapping

You can override the inherited role by passing a second argument to extend.

This can be used to give users limited access to a child group:

const organization = Group.create(); const billing = Group.create(); billing.extend(organization, "reader");

This way the members of the organization can only read the billing data, even if they are admins in the organization group.

More about the group inheritance can be found in the dedicated docs page.