# Jazz
## Documentation
### Getting started
#### Introduction
Welcome to the Jazz documentation!
The Jazz docs are currently heavily work in progress, sorry about that!
## Quickstart
Run the following command to create a new Jazz project from one of our example apps:
```sh
npx create-jazz-app@latest
```
Or set up Jazz yourself, using the following instructions for your framework of choice:
- [React](/docs/react/project-setup)
- [Next.js](/docs/react/project-setup#nextjs)
- [React Native](/docs/react-native/project-setup)
- [Vue](/docs/vue/project-setup)
- [Svelte](/docs/svelte/project-setup)
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
## Example apps
You can also find [example apps](/examples) with code most similar to what you want to build. These apps
make use of different features such as auth, file upload, and more.
## Sync and storage
Sync and persist your data by setting up a [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or do it yourself.
## Collaborative values
Learn how to structure your data using [collaborative values](/docs/schemas/covalues).
## API Reference
Many of the packages provided are documented in the [API Reference](/api-reference).
## LLM Docs
We support the [llms.txt](https://llmstxt.org/) convention for making documentation available to large language models and the applications that make use of them.
We currently have:
- [/llms.txt](/llms.txt) - A overview listing of the available packages and their documentation
- [/llms-full.txt](/llms-full.txt) - Full documentation for our packages
## Get support
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42).
We would love to help you get started.
#### Guide
### react Implementation
# React guide
This is a step-by-step tutorial where we'll build an issue tracker app using React.
You'll learn how to set up a Jazz app, use Jazz Cloud for sync and storage, create and manipulate data using
Collaborative Values (CoValues), build a UI and subscribe to changes, set permissions, and send invites.
## Project setup
1. Create a project called "circular" from a generic Vite starter template:
{/* prettier-ignore */}
```bash
npx degit gardencmp/vite-ts-react-tailwind circular
cd circular
npm install
npm run dev
```
You should now have an empty app running, typically at [localhost:5173](http://localhost:5173).
(If you make changes to the code, the app will automatically refresh.)
2. Install `jazz-tools` and `jazz-react`
(in a new terminal window):
{/* prettier-ignore */}
```bash
cd circular
npm install jazz-tools jazz-react
```
3. Modify `src/main.tsx` to set up a Jazz context:
{/* prettier-ignore */}
```tsx
import React from "react"; // old
import ReactDOM from "react-dom/client"; // old
import App from "./App.tsx"; // old
import "./index.css"; // old
import {
JazzProvider,
useDemoAuth,
DemoAuthBasicUI,
} from "jazz-react";
// old
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, authState] = useDemoAuth();
return (
<>
{children}
>
);
}
// old
ReactDOM.createRoot(document.getElementById("root")!).render( // old
// old
// old
); // old
```
This sets Jazz up and wraps our app in the provider.
{/* TODO: explain Auth */}
## Intro to CoValues
Let's learn about the **central idea** behind Jazz: **Collaborative Values.**
What if we could **treat distributed state like local state?** That's what CoValues do.
We can
- **create** CoValues, anywhere
- **load** CoValues by `ID`, from anywhere else
- **edit** CoValues, from anywhere, by mutating them like local state
- **subscribe to edits** in CoValues, whether they're local or remote
### Declaring our own CoValues
To make our own CoValues, we first need to declare a schema for them. Think of a schema as a combination of TypeScript types and runtime type information.
Let's start by defining a schema for our most central entity in Circular: an **Issue.**
Create a new file `src/schema.ts` and add the following:
```ts
export class Issue extends CoMap {
title = co.string;
description = co.string;
estimate = co.number;
status? = co.literal("backlog", "in progress", "done");
}
```
{/* TODO: explain what's happening */}
### Reading from CoValues
CoValues are designed to be read like simple local JSON state. Let's see how we can read from an Issue by building a component to render one.
Create a new file `src/components/Issue.tsx` and add the following:
{/* prettier-ignore */}
```tsx
export function IssueComponent({ issue }: { issue: Issue }) {
return (
{issue.title}
{issue.description}
Estimate: {issue.estimate}
Status: {issue.status}
);
}
```
Simple enough!
### Creating CoValues
To actually see an Issue, we have to create one. This is where things start to get interesting...
Let's modify `src/App.tsx` to prepare for creating an Issue and then rendering it:
{/* prettier-ignore */}
```tsx
// old
function App() {// old
const [issue, setIssue] = useState();
// old
if (issue) {
return ;
} else {
return ;
}
} // old
// old
export default App; // old
```
Now, finally, let's implement creating an issue:
{/* prettier-ignore */}
```tsx
// old
function App() {// old
const [issue, setIssue] = useState(); // old
// old
const createIssue = () => {
const newIssue = Issue.create(
{
title: "Buy terrarium",
description: "Make sure it's big enough for 10 snails.",
estimate: 5,
status: "backlog",
},
);
setIssue(newIssue);
};
// old
if (issue) {// old
return ; // old
} else { // old
return ;
} // old
} // old
// old
export default App; // old
```
π Now you should be able to create a new issue by clicking the button and then see it rendered!
Preview
Buy terrarium
Make sure it's big enough for 10 snails.
Estimate: 5
Status: backlog
We'll already notice one interesting thing here:
- We have to create every CoValue with an `owner`!
- this will determine access rights on the CoValue, which we'll learn about in "Groups & Permissions"
- here the `owner` is set automatically to a group managed by the current user because we have not declared any
**Behind the scenes, Jazz not only creates the Issue in memory but also automatically syncs an encrypted version to the cloud and persists it locally. The Issue also has a globally unique ID.**
We'll make use of both of these facts in a bit, but for now let's start with local editing and subscribing.
### Editing CoValues and subscribing to edits
Since we're the owner of the CoValue, we should be able to edit it, right?
And since this is a React app, it would be nice to subscribe to edits of the CoValue and reactively re-render the UI, like we can with local state.
This is exactly what the `useCoState` hook is for!
- Note that `useCoState` doesn't take a CoValue directly, but rather a CoValue's schema, plus its `ID`.
- So we'll slightly adapt our `useState` to only keep track of an issue ID...
- ...and then use `useCoState` to get the actual issue
Let's modify `src/App.tsx`:
{/* prettier-ignore */}
```tsx
// old
function App() { // old
const [issueID, setIssueID] = useState>();
// old
const issue = useCoState(Issue, issueID);
// old
const createIssue = () => {// old
const newIssue = Issue.create(// old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
); // old
setIssueID(newIssue.id);
}; // old
// old
if (issue) { // old
return ; // old
} else { // old
return ; // old
} // old
} // old
// old
export default App; // old
```
And now for the exciting part! Let's make `src/components/Issue.tsx` an editing component.
{/* prettier-ignore */}
```tsx
// old
export function IssueComponent({ issue }: { issue: Issue }) { // old
return ( // old
// old
{ issue.title = event.target.value }}/>
// old
); // old
} // old
```
Preview
π Now you should be able to edit the issue after creating it!
You'll immediately notice that we're doing something non-idiomatic for React: we mutate the issue directly, by assigning to its properties.
This works because CoValues
- intercept these edits
- update their local view accordingly (React doesn't really care after rendering)
- notify subscribers of the change (who will receive a fresh, updated view of the CoValue)
We have one subscriber on our Issue, with `useCoState` in `src/App.tsx`, which will cause the `App` component and its children **to** re-render whenever the Issue changes.
### Automatic local & cloud persistence
So far our Issue CoValues just looked like ephemeral local state. We'll now start exploring the first main feature that makes CoValues special: **automatic persistence.**
Actually, all the Issue CoValues we've created so far **have already been automatically persisted** to the cloud and locally - but we lose track of their ID after a reload.
So let's store the ID in the browser's URL and make sure our useState is in sync with that.
{/* prettier-ignore */}
```tsx
// old
function App() { // old
const [issueID, setIssueID] = useState | undefined>(
(window.location.search?.replace("?issue=", "") || undefined) as ID | undefined,
);
// old
const issue = useCoState(Issue, issueID); // old
// old
const createIssue = () => {// old
const newIssue = Issue.create(// old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
); // old
setIssueID(newIssue.id); // old
window.history.pushState({}, "", `?issue=${newIssue.id}`);
}; // old
// old
if (issue) { // old
return ; // old
} else { // old
return ; // old
} // old
} // old
// old
export default App; // old
```
π Now you should be able to create an issue, edit it, reload the page, and still see the same issue.
### Remote sync
To see that sync is also already working, try the following:
- copy the URL to a new tab in the same browser window and see the same issue
- edit the issue and see the changes reflected in the other tab!
This works because we load the issue as the same account that created it and owns it (remember how you set `{ owner: me }`).
But how can we share an Issue with someone else?
### Simple public sharing
We'll learn more about access control in "Groups & Permissions", but for now let's build a super simple way of sharing an Issue by just making it publicly readable & writable.
All we have to do is create a new group to own each new issue and add "everyone" as a "writer":
{/* prettier-ignore */}
```tsx
// old
function App() { // old
const { me } = useAccount(); // old
const [issueID, setIssueID] = useState | undefined>(// old
(window.location.search?.replace("?issue=", "") || undefined) as ID | undefined,// old
); // old
// old
const issue = useCoState(Issue, issueID); // old
// old
const createIssue = () => { // old
const group = Group.create({ owner: me });
group.addMember("everyone", "writer");
// old
const newIssue = Issue.create( // old
{ // old
title: "Buy terrarium", // old
description: "Make sure it's big enough for 10 snails.", // old
estimate: 5, // old
status: "backlog", // old
}, // old
{ owner: group },
); // old
setIssueID(newIssue.id); // old
window.history.pushState({}, "", `?issue=${newIssue.id}`); // old
}; // old
// old
if (issue) { // old
return ; // old
} else { // old
return ; // old
} // old
} // old
// old
export default App; // old
```
π Now you should be able to open the Issue (with its unique URL) on another device or browser, or send it to a friend and you should be able to **edit it together in realtime!**
This concludes our intro to the essence of CoValues. Hopefully you're starting to have a feeling for how CoValues behave and how they're magically available everywhere.
## Refs & auto-subscribe
Now let's have a look at how to compose CoValues into more complex structures and build a whole app around them.
Let's extend our two data model to include "Projects" which have a list of tasks and some properties of their own.
Using plain objects, you would probably type a Project like this:
```ts
type Project = {
name: string;
issues: Issue[];
};
```
In order to create this more complex structure in a fully collaborative way, we're going to need _references_ that allow us to nest or link CoValues.
Add the following to `src/schema.ts`:
```ts
// old
export class Issue extends CoMap { // old
title = co.string; // old
description = co.string; // old
estimate = co.number; // old
status? = co.literal("backlog", "in progress", "done"); // old
} // old
// old
export class ListOfIssues extends CoList.Of(co.ref(Issue)) {}
export class Project extends CoMap {
name = co.string;
issues = co.ref(ListOfIssues);
}
```
Now let's change things up a bit in terms of components as well.
First, we'll change `App.tsx` to create and render `Project`s instead of `Issue`s. (We'll move the `useCoState` into the `ProjectComponent` we'll create in a second).
{/* prettier-ignore */}
```tsx
// old
function App() { // old
const [projectID, setProjectID] = useState | undefined>(
(window.location.search?.replace("?project=", "") || undefined) as ID | undefined
);
// old
const issue = useCoState(Issue, issueID); // *bin*
// old
const createProject = () => {
const group = Group.create();
group.addMember("everyone", "writer");
const newProject = Project.create(
{
name: "New Project",
issues: ListOfIssues.create([], { owner: group })
},
group,
);
setProjectID(newProject.id);
window.history.pushState({}, "", `?project=${newProject.id}`);
};
// old
if (projectID) {
return ;
} else {
return ;
}
} // old
// old
export default App; // old
```
Now we'll actually create the `ProjectComponent` that renders a `Project` and its `Issue`s.
Create a new file `src/components/Project.tsx` and add the following:
{/* prettier-ignore */}
```tsx
export function ProjectComponent({ projectID }: { projectID: ID }) {
const project = useCoState(Project, projectID);
const createAndAddIssue = () => {
project?.issues?.push(Issue.create({
title: "",
description: "",
estimate: 0,
status: "backlog",
}, project._owner));
};
return project ? (
{project.name}
{project.issues?.map((issue) => (
issue &&
))}
) : (
Loading project...
);
}
```
π Now you should be able to create a project, add issues to it, share it, and edit it collaboratively!
Two things to note here:
- We create a new Issue like before, and then push it into the `issues` list of the Project. By setting the `owner` to the Project's owner, we ensure that the Issue has the same access rights as the project itself.
- We only need to use `useCoState` on the Project, and the nested `ListOfIssues` and each `Issue` will be **automatically loaded and subscribed to when we access them.**
- However, because either the `Project`, `ListOfIssues`, or each `Issue` might not be loaded yet, we have to check for them being defined.
### Precise loading depths
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
But you can also take more precise control over loading by defining a minimum-depth to load in `useCoState`:
{/* prettier-ignore */}
```tsx
// old
export function ProjectComponent({ projectID }: { projectID: ID }) {// old
const project = useCoState(Project, projectID, { issues: [{}] });
const createAndAddIssue = () => {// old
project?.issues.push(Issue.create({
title: "",// old
description: "",// old
estimate: 0,// old
status: "backlog",// old
}, project._owner));// old
};// old
// old
return project ? (// old
// old
{project.name}
// old
// old
{project.issues.map((issue) => (
))}// old
// old
// old
// old
) : (// old
Loading project...
// old
);// old
}// old
```
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
- Now, we can get rid of a lot of coniditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
{/* TODO: explain about not loaded vs not set/defined and `_refs` basics */}
## Groups & permissions
We've seen briefly how we can use Groups to give everyone access to a Project,
and how we can use `{ owner: me }` to make something private to the current user.
### Groups / Accounts as permission scopes
This gives us a hint of how permissions work in Jazz: **every CoValue has an owner,
and the access rights on that CoValue are determined by its owner.**
- If the owner is an Account, only that Account can read and write the CoValue.
- If the owner is a Group, the access rights depend on the *role* of the Account (that is trying to access the CoValue) in that Group.
- `"reader"`s can read but not write to CoValues belonging to the Group.
- `"writer"`s can read and write to CoValues belonging to the Group.
- `"admin"`s can read and write to CoValues belonging to the Group *and can add and remove other members from the Group itself.*
### Creating invites
There is also an abstraction for creating *invitations to join a Group* (with a specific role) that you can use
to add people without having to know their Account ID.
Let's use these abstractions to build teams for a Project that we can invite people to.
Turns out, we're already mostly there! First, let's remove making the Project public:
{/* prettier-ignore */}
```tsx
// old
function App() { // old
const [projectID, setProjectID] = useState | undefined>( // old
(window.location.search?.replace("?project=", "") || undefined) as ID | undefined, // old
); // old
// old
const createProject = () => { // old
const group = Group.create(); // old
group.addMember("everyone", "writer"); // *bin*
// old
const newProject = Project.create( // old
{ // old
name: "New Project", // old
issues: ListOfIssues.create([], { owner: group }) // old
}, // old
group, // old
); // old
setProjectID(newProject.id); // old
window.history.pushState({}, "", `?project=${newProject.id}`); // old
}; // old
// old
if (projectID) { // old
return ; // old
} else { // old
return ; // old
} // old
} // old
// old
export default App; // old
```
Now, inside ProjectComponent, let's add a button to invite guests (read-only) or members (read-write) to the Project.
{/* prettier-ignore */}
```tsx
// old
export function ProjectComponent({ projectID }: { projectID: ID }) {// old
const project = useCoState(Project, projectID, { issues: [{}] }); // old
const invite = (role: "reader" | "writer") => {
const link = createInviteLink(project, role, { valueHint: "project" });
navigator.clipboard.writeText(link);
};
const createAndAddIssue = () => {// old
project?.issues.push(Issue.create({ // old
title: "",// old
description: "",// old
estimate: 0,// old
status: "backlog",// old
}, project._owner));// old
};// old
// old
return project ? (// old
// old
{project.name}
// old
{project._owner?.myRole() === "admin" && (
<>
>
)}
// old
{project.issues.map((issue) => ( // old
// old
))}// old
// old
// old
// old
) : (// old
Loading project...
// old
);// old
}// old
```
### Consuming invites
#### Example apps
### Project setup
#### Installation
### react-native Implementation
# React Native
Jazz requires an [Expo development build](https://docs.expo.dev/develop/development-builds/introduction/) using [Expo Prebuild](https://docs.expo.dev/workflow/prebuild/) for native code. It is **not compatible** with Expo Go. Jazz also supports the [New Architecture](https://docs.expo.dev/guides/new-architecture/).
Tested with:
```json
"expo": "~51.0.0",
"react-native": "~0.74.5",
"react": "^18.2.0",
```
## Setup
### Create a new project
(skip this step if you already have one)
```bash
npx create-expo-app -e with-router-tailwind my-jazz-app
cd my-jazz-app
npx expo prebuild
```
### Install dependencies
```bash
npx expo install expo-linking expo-secure-store expo-file-system @react-native-community/netinfo @bam.tech/react-native-image-resizer @azure/core-asynciterator-polyfill
npm i -S react-native-polyfill-globals react-native-url-polyfill web-streams-polyfill@3.2.1 base-64 text-encoding react-native-fetch-api react-native-get-random-values buffer @op-engineering/op-sqlite
npm i -S jazz-tools jazz-react-native jazz-react-native-media-images
```
### Fix incompatible dependencies
```bash
npx expo install --fix
```
### Install Pods
```bash
npx pod-install
```
### Configure Metro
#### Regular repositories
If you are not working within a monorepo, create a new file metro.config.js in the root of your project with the following content:
```ts
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(projectRoot);
config.resolver.unstable_enablePackageExports = true; // important setting
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
module.exports = config;
```
If you created the project using the command `npx create-expo-app -e with-router-tailwind my-jazz-app`, then `metro.config.js` is already present. In that case, simply add this setting to the existing file:
```ts
config.resolver.unstable_enablePackageExports = true
```
#### Monorepos
For monorepos, use the following metro.config.js:
```ts
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
const path = require("path");
// eslint-disable-next-line no-undef
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "../..");
const config = getDefaultConfig(projectRoot);
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
config.resolver.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.unstable_enablePackageExports = true;
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
config.cacheStores = [
new FileStore({
root: path.join(projectRoot, "node_modules", ".cache", "metro"),
}),
];
module.exports = config;
```
### Additional monorepo configuration (for pnpm users)
- Add node-linker=hoisted to the root .npmrc (create this file if it doesnβt exist).
- Add the following to the root package.json:
```json
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@babel/*",
"expo-modules-*",
"typescript"
]
}
}
```
For more information, refer to [this](https://github.com/byCedric/expo-monorepo-example#pnpm-workarounds) Expo monorepo example.
### Add polyfills
Create a file `polyfills.js` at the project root with the following content:
```ts
import "react-native-polyfill-globals/auto";
import "@azure/core-asynciterator-polyfill";
import { ReadableStream } from "web-streams-polyfill/ponyfill/es6";
import { polyfillGlobal } from "react-native/Libraries/Utilities/PolyfillFunctions";
import { Buffer } from "buffer";
polyfillGlobal("Buffer", () => Buffer);
polyfillGlobal("ReadableStream", () => ReadableStream);
```
Update `index.js` based on whether you are using expo-router or not:
#### If using `expo-router`
```ts
import "./polyfills";
import "expo-router/entry";
```
#### Without `expo-router`
```ts
import "./polyfills";
import { registerRootComponent } from "expo";
import App from "./src/App";
registerRootComponent(App);
```
Lastly, ensure that the `"main"` field in your `package.json` points to `index.js`:
```json
"main": "index.js",
```
## Setting up the provider
Wrap your app components with the `JazzProvider:
```tsx
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
import { MyAppAccount } from "./schema";
export function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [auth, state] = useDemoAuth();
return (
<>
{children}
>
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react-native" {
interface Register {
Account: MyAppAccount;
}
}
```
You can optionally pass a custom `kvStore` and `AccountSchema` to `createJazzRNApp()`, otherwise, it defaults to `ExpoSecureStoreAdapter` and `Account`.
### Choosing an auth method
Refer to the Jazz + React Native demo projects for implementing authentication:
- [DemoAuth Example](https://github.com/garden-co/jazz/tree/main/examples/chat-rn)
- [ClerkAuth Example](https://github.com/garden-co/jazz/tree/main/examples/chat-rn-clerk)
In the demos, you'll find details on:
- Using JazzProvider with your chosen authentication method
- Defining a Jazz schema
- Creating and subscribing to covalues
- Handling invites
### Working with Images
Jazz provides a complete solution for handling images in React Native, including uploading, processing, and displaying them. Here's how to work with images:
#### Uploading Images
To upload images, use the `createImage` function from `jazz-react-native-media-images`. This function handles image processing and creates an `ImageDefinition` that can be stored in your Jazz covalues:
```tsx
import { createImage } from "jazz-react-native-media-images";
import * as ImagePicker from 'expo-image-picker';
// Example: Image upload from device library
const handleImageUpload = async () => {
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
base64: true, // Important: We need base64 data
quality: 0.7,
});
if (!result.canceled && result.assets[0].base64) {
const base64Uri = `data:image/jpeg;base64,${result.assets[0].base64}`;
const image = await createImage(base64Uri, {
owner: someCovalue._owner, // Set appropriate owner
maxSize: 2048, // Optional: limit maximum image size
});
// Store the image in your covalue
someCovalue.image = image;
}
} catch (error) {
console.error('Failed to upload image:', error);
}
};
```
#### Displaying Images
To display images, use the `ProgressiveImg` component from `jazz-react-native`. This component handles both images uploaded from React Native and desktop browsers:
```tsx
import { ProgressiveImg } from "jazz-react-native";
import { Image } from "react-native";
// Inside your render function:
{({ src, res, originalSize }) => (
)}
```
The `ProgressiveImg` component:
- Automatically handles different image formats
- Provides progressive loading with placeholder images
- Supports different resolutions based on the `maxWidth` prop
- Works seamlessly with React Native's `Image` component
For a complete implementation example, see the [Chat Example](https://github.com/garden-co/jazz/blob/main/examples/chat-rn-clerk/app/chat/[chatId].tsx).
### Running your app
```bash
npx expo run:ios
npx expo run:android
```
---
### react Implementation
# React
First you define a context providing component (typically called `JazzAndAuth`) that uses:
- the Jazz context provider with your app configs
- the hooks and default/custom UI of one of the [Auth Methods](/docs/react/authentication/auth-methods).
This is also where you specify the sync & storage server to connect to (see [Sync and storage](/docs/react/sync-and-storage)).
{/* prettier-ignore */}
```tsx
import { JazzProvider } from "jazz-react";// old
import { MyAppAccount } from "./schema";
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName });
return (
<>
{children}
>
);
}
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" {
interface Register {
Account: MyAppAccount;
}
}
```
With `JazzAndAuth` defined, you can wrap your app in it and then use the hooks within your App.
{/* prettier-ignore */}
```tsx
ReactDOM.createRoot(document.getElementById("root")!).render( // old
// old
// old
);// old
```
## Next.js
### Client-side only
The easiest way to use Jazz with Next.JS is to only use it on the client side. You can ensure this by:
- marking the Jazz provider file as `"use client"`
{/* prettier-ignore */}
```tsx
"use client"
import { JazzProvider } from "jazz-react"; // old
import { PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";// old
import { MyAppAccount } from "./schema"; // old
function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
return ( // old
<> // old
{children} // old
// old
// old
> // old
); // old
} // old
```
- marking any file with components where you use Jazz hooks (such as `useAccount` or `useCoState`) as `"use client"`
### SSR use (experimental)
Pure SSR use of Jazz is basically just using jazz-nodejs (see [Node.JS / Server Workers](/docs/react/project-setup/server-side)) inside Server Components.
Instead of using hooks as you would on the client, you await promises returned by `CoValue.load(...)` inside your Server Components.
TODO: code example
This should work well for cases like rendering publicly-readable information, since the worker account will be able to load them.
In the future, it will be possible to use trusted auth methods (such as Clerk, Auth0, etc.) that let you act as the same Jazz user both on the client and on the server, letting you use SSR even for data private to that user.
### SSR + client-side (experimental)
You can combine the two approaches by creating
1. A pure "rendering" component that renders an already-loaded CoValue (in JSON-ified form)
TODO: code example
2. A "hydrating" component (with `"use client"`) that
- expects a pre-loaded CoValue as a prop (in JSON-ified form)
- uses one of the client-side Jazz hooks (such as `useAccount` or `useCoState`) to subscribe to that same CoValue
- passing the client-side subscribed state to the "rendering" component, with the pre-loaded CoValue as a fallback until the client receives the first subscribed state
TODO: code example
3. A "pre-loading" Server Component that
- pre-loads the CoValue by awaiting it's `load(...)` method (as described above)
- renders the "hydrating" component, passing the pre-loaded CoValue as a prop
TODO: code example
---
### server-side Implementation
# Node.JS / server workers
The main detail to understand when using Jazz server-side is that Server Workers have Jazz `Accounts`, just like normal users do.
This lets you share CoValues with Server Workers, having precise access control by adding the Worker to `Groups` with specific roles just like you would with other users.
## Generating credentials
Server Workers typically have static credentials, consisting of a public Account ID and a private Account Secret.
To generate new credentials for a Server Worker, you can run:
{/* prettier-ignore */}
```sh
npx jazz-run account create --name "My Server Worker"
```
The name will be put in the public profile of the Server Worker's `Account`, which can be helpful when inspecting metadata of CoValue edits that the Server Worker has done.
## Storing & providing credentials
Server Worker credentials are typically stored and provided as environmental variables.
**Take extra care with the Account Secret — handle it like any other secret environment variable such as a DB password.**
## Starting a server worker
You can use `startWorker` from `jazz-nodejs` to start a Server Worker. Similarly to setting up a client-side Jazz context, it:
- takes a custom `AccountSchema` if you have one (for example, because the worker needs to store information in it's private account root)
- takes a URL for a sync & storage server
`startWorker` expects credentials in the `JAZZ_WORKER_ACCOUNT` and `JAZZ_WORKER_SECRET` environment variables by default (as printed by `npx account create ...`), but you can also pass them manually as `accountID` and `accountSecret` parameters if you get them from elsewhere.
{/* prettier-ignore */}
```ts
const { worker } = await startWorker({
AccountSchema: MyWorkerAccount,
syncServer: 'wss://cloud.jazz.tools/?key=you@example.com',
});
```
`worker` acts like `me` (as returned by `useAccount` on the client) - you can use it to:
- load/subscribe to CoValues: `MyCoValue.subscribe(id, worker, {...})`
- create CoValues & Groups `const val = MyCoValue.create({...}, { owner: worker })`
## Using CoValues instead of requests
Just like traditional backend functions, you can use Server Workers to do useful stuff (computations, calls to third-party APIs etc.) and put the results back into CoValues, which subscribed clients automatically get notified about.
What's less clear is how you can trigger this work to happen.
- One option is to define traditional HTTP API handlers that use the Jazz Worker internally. This is helpful if you need to mutate Jazz state in response to HTTP requests such as for webhooks or non-Jazz API clients
- The other option is to have the Jazz Worker subscribe to CoValues which they will then collaborate on with clients.
- A common pattern is to implement a state machine represented by a CoValue, where the client will do some state transitions (such as `draft -> ready`), which the worker will notice and then do some work in response, feeding the result back in a further state transition (such as `ready -> success & data`, or `ready -> failure & error details`).
- This way, client and worker don't have to explicitly know about each other or communicate directly, but can rely on Jazz as a communication mechanism - with computation progressing in a distributed manner wherever and whenever possible.
---
### svelte Implementation
# Svelte Installation
Jazz can be used with Svelte or in a SvelteKit app.
To add some Jazz to your Svelte app, you can use the following steps:
1. Install Jazz dependencies
```sh
pnpm install jazz-tools jazz-svelte
```
2. Write your schema
See the [schema docs](/docs/schemas/covalues) for more information.
```ts
// src/lib/schema.ts
export class MyProfile extends Profile {
name = co.string;
counter = co.number; // This will be publically visible
}
export class MyAccount extends Account {
profile = co.ref(MyProfile);
// ...
}
```
3. Set up the Provider in your root layout
```svelte
```
4. Use Jazz hooks in your components
```svelte
```
For a complete example of Jazz with Svelte, check out our [file sharing example](https://github.com/gardencmp/jazz/tree/main/examples/file-share-svelte) which demonstrates, Passkey authentication, file uploads and access control.
---
### vue Implementation
# VueJS demo todo app guide
This guide provides step-by-step instructions for setting up and running a Jazz-powered Todo application using VueJS.
See the full example [here](https://github.com/garden-co/jazz/tree/main/examples/todo-vue).
---
## Setup
### Create a new app
Run the following command to create a new VueJS application:
```bash
β― pnpm create vue@latest
β Project name: β¦ vue-setup-guide
β Add TypeScript? β¦ Yes
β Add JSX Support? β¦ No
β Add Vue Router for Single Page Application development? β¦ Yes
β Add Pinia for state management? β¦ No
β Add Vitest for Unit Testing? β¦ No
β Add an End-to-End Testing Solution? βΊ No
β Add ESLint for code quality? βΊ Yes
β Add Prettier for code formatting? β¦ Yes
```
### Install dependencies
Run the following command to install Jazz libraries:
```bash
pnpm install jazz-tools jazz-browser jazz-vue
```
### Implement `schema.ts`
Define the schema for your application.
Example schema inside `src/schema.ts` for a todo app:
```typescript
export class ToDoItem extends CoMap {
name = co.string;
completed = co.boolean;
}
export class ToDoList extends CoList.Of(co.ref(ToDoItem)) {}
export class Folder extends CoMap {
name = co.string;
items = co.ref(ToDoList);
}
export class FolderList extends CoList.Of(co.ref(Folder)) {}
export class ToDoAccountRoot extends CoMap {
folders = co.ref(FolderList);
}
export class ToDoAccount extends Account {
profile = co.ref(Profile);
root = co.ref(ToDoAccountRoot);
migrate() {
if (!this._refs.root) {
const group = Group.create({ owner: this });
const firstFolder = Folder.create(
{
name: "Default",
items: ToDoList.create([], { owner: group }),
},
{ owner: group },
);
this.root = ToDoAccountRoot.create(
{
folders: FolderList.create([firstFolder], {
owner: this,
}),
},
{ owner: this },
);
}
}
}
```
### Refactor `main.ts`
Update the `src/main.ts` file to integrate Jazz:
```typescript
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const RootComponent = defineComponent({
name: "RootComponent",
setup() {
const { authMethod, state } = useDemoAuth();
return () => [
h(
JazzProvider,
{
AccountSchema: ToDoAccount,
auth: authMethod.value,
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
{
default: () => h(App),
},
),
state.state !== "signedIn" &&
h(DemoAuthBasicUI, {
appName: "Jazz Vue Todo",
state,
}),
];
},
});
const app = createApp(RootComponent);
app.use(router);
app.mount("#app");
```
### Set up `router/index.ts`:
Create a basic Vue router configuration. For example:
```typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "Home",
component: HomeView,
},
],
});
export default router;
```
### Implement `App.vue`
Update the `App.vue` file to include logout functionality:
```typescript
Todo App
{{ me.profile?.name }}
```
## Subscribing to a CoValue
Subscribe to a CoValue inside `src/views/HomeView.vue`:
```typescript
```
Top level imports for hooks
All Jazz hooks are now available as top-level imports from the `jazz-svelte` package.
This change improves IDE intellisense support and simplifies imports:
{/* prettier-ignore */}
```svelte
Hello {me.profile?.name}
```
New testing utilities
Removing `createJazzApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
{/* prettier-ignore */}
```ts
import { useCoState } from "jazz-svelte";
import { createJazzTestAccount, JazzTestProvider } from "jazz-svelte/testing";
import { render } from "@testing-library/svelte"; // old
import { Playlist, MusicAccount } from "./schema"; // old
test("should load the playlist", async () => {
// β Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// β Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// β Use createJazzTestContext in your tests
render(PlaylistComponent, {
context: createJazzTestContext({ account: options.account }),
props: {
id: playlist.id,
},
});
expect(await screen.findByRole("heading", { name: "My playlist" })).toBeInTheDocument();
});
```
Version 0.9.0 simplifies the application setup and makes Jazz more intellisense friendly by
replacing the `createJazzVueApp` API with top-level imports.
We have also introduced some new API to make testing Jazz components a breeze. π¬οΈ
New provider setup
The `JazzProvider` is now imported from `jazz-vue` instead of `createJazzVueApp`.
While `createJazzReactApp` was originally designed to setup strong typing for custom Account schemas in `useAccount`,
we found that this approach made the Jazz setup awkward and confusing for some users.
So we decided to remove `createJazzReactApp` step and to provide the types through namespace declarations:
{/* prettier-ignore */}
```typescript
// Remove these lines // *bin*
const Jazz = createJazzVueApp({ AccountSchema: ToDoAccount }); // *bin*
export const { useAccount, useCoState } = Jazz; // *bin*
const { JazzProvider } = Jazz; // *bin*
const RootComponent = defineComponent({ // old
name: "RootComponent", // old
setup() { // old
const { authMethod, state } = useDemoAuth(); // old
return () => [ // old
h( // old
JazzProvider, // old
{ // old
AccountSchema: ToDoAccount, // The custom Account schema is passed here now
auth: authMethod.value, // old
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co", // old
}, // old
{ // old
default: () => h(App), // old
}, // old
), // old
state.state !== "signedIn" && // old
h(DemoAuthBasicUI, { // old
appName: "Jazz Vue Todo", // old
state, // old
}), // old
]; // old
}, // old
}); // old
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-vue" {
interface Register {
Account: ToDoAccount;
}
}
const app = createApp(RootComponent); // old
app.use(router); // old
app.mount("#app"); // old
```
Top level imports for hooks
All Jazz hooks are now available as top-level imports from the `jazz-vue` package.
This change improves IDE intellisense support and simplifies imports:
Removing `createJazzTestApp` also makes testing way easier!
We can now use `createJazzTestAccount` to setup accounts and testing data and pass it to
your components and hooks using `JazzTestProvider`:
{/* prettier-ignore */}
```tsx
import { createJazzTestAccount, JazzTestProvider } from "jazz-vue/testing";
import { createApp, defineComponent, h } from "vue";
import { usePlaylist } from "./usePlaylist";
import { Playlist, MusicAccount } from "./schema"; // old
// This can be reused on other tests!
export const renderComposableWithJazz = any>(
composable: C,
{ account }: { account: Account | { guest: AnonymousJazzAgent } },
) => {
let result;
const wrapper = defineComponent({
setup() {
result = composable();
// suppress missing template warning
return () => {};
},
});
// β Use JazzTestProvider in your tests
const app = createApp({
setup() {
return () =>
h(
JazzTestProvider,
{
account,
},
{
default: () => h(wrapper),
},
);
},
});
app.mount(document.createElement("div"));
return [result, app] as [ReturnType, ReturnType];
};
test("should load the playlist", async () => {
// β Create a test account with your schema
const account = await createJazzTestAccount({ AccountSchema: MusicAccount });
// β Set up test data
const playlist = Playlist.create({
name: "My playlist",
}, account);
// β Set up test data
const { result } = renderComposableWithJazz(() => usePlaylist(playlist.id), {
account,
});
// The result is resolved synchronously, so you can assert the value immediately
expect(result?.name).toBe("My playlist");
});
```
### Defining schemas
#### CoValues
# Defining schemas: CoValues
**CoValues ("Collaborative Values") are the core abstraction of Jazz.** They're your bread-and-butter datastructures that you use to represent everything in your app.
As their name suggests, CoValues are inherently collaborative, meaning **multiple users and devices can edit them at the same time.**
**Think of CoValues as "super-fast Git for lots of tiny data."**
- CoValues keep their full edit histories, from which they derive their "current state".
- The fact that this happens in an eventually-consistent way makes them [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type).
- Having the full history also means that you often don't need explicit timestamps and author info - you get this for free as part of a CoValue's [edit metadata](/docs/using-covalues/metadata).
CoValues model JSON with CoMaps and CoLists, but also offer CoFeeds for simple per-user value feeds, and let you represent binary data with FileStreams.
## Start your app with a schema
Fundamentally, CoValues are as dynamic and flexible as JSON, but in Jazz you use them by defining fixed schemas to describe the shape of data in your app.
This helps correctness and development speed, but is particularly important...
- when you evolve your app and need migrations
- when different clients and server workers collaborate on CoValues and need to make compatible changes
Thinking about the shape of your data is also a great first step to model your app.
Even before you know the details of how your app will work, you'll probably know which kinds of objects it will deal with, and how they relate to each other.
Jazz makes it quick to declare schemas, since they are simple TypeScript classes:
{/* prettier-ignore */}
```ts
export class TodoProject extends CoMap {
title = co.string;
tasks = co.ref(ListOfTasks);
}
```
Here you can see how we extend a CoValue type and use `co` for declaring (collaboratively) editable fields. This means that schema info is available for type inference *and* at runtime.
Classes might look old-fashioned, but Jazz makes use of them being both types and values in TypeScript, letting you refer to either with a single definition and import.
{/* prettier-ignore */}
```ts
const project: TodoProject = TodoProject.create(
{
title: "New Project",
tasks: ListOfTasks.create([], Group.create()),
},
Group.create()
);
```
## Types of CoValues
### `CoMap` (declaration)
CoMaps are the most commonly used type of CoValue. They are the equivalent of JSON objects. (Collaborative editing follows a last-write-wins strategy per-key.)
You can either declare struct-like CoMaps:
{/* prettier-ignore */}
```ts
class Person extends CoMap {
name = co.string;
age = co.number;
pet = co.optional.ref(Pet);
}
```
Or record-like CoMaps (key-value pairs, where keys are always `string`):
{/* prettier-ignore */}
```ts
class ColorToHex extends CoMap.Record(co.string) {}
class ColorToFruit extends CoMap.Record(co.ref(Fruit)) {}
```
See the corresponding sections for [creating](/docs/using-covalues/creation#comap-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#comap-reading) and
[writing to](/docs/using-covalues/writing#comap-writing) CoMaps.
### `CoList` (declaration)
CoLists are ordered lists and are the equivalent of JSON arrays. (They support concurrent insertions and deletions, maintaining a consistent order.)
You define them by specifying the type of the items they contain:
{/* prettier-ignore */}
```ts
class ListOfColors extends CoList.Of(co.string) {}
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
```
See the corresponding sections for [creating](/docs/using-covalues/creation#colist-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#colist-reading) and
[writing to](/docs/using-covalues/writing#colist-writing) CoLists.
### `CoFeed` (declaration)
CoFeeds are a special CoValue type that represent a feed of values for a set of users / sessions. (Each session of a user gets its own append-only feed.)
They allow easy access of the latest or all items belonging to a user or their sessions. This makes them particularly useful for user presence, reactions, notifications, etc.
You define them by specifying the type of feed item:
{/* prettier-ignore */}
```ts
class FeedOfTasks extends CoFeed.Of(co.ref(Task)) {}
```
See the corresponding sections for [creating](/docs/using-covalues/creation#cofeed-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#cofeed-reading) and
[writing to](/docs/using-covalues/writing#cofeed-writing) CoFeeds.
### `FileStream` (declaration)
FileStreams are a special type of CoValue that represent binary data. (They are created by a single user and offer no internal collaboration.)
They allow you to upload and reference files, images, etc.
You typically don't need to declare or extend them yourself, you simply refer to the built-in `FileStream` from another CoValue:
{/* prettier-ignore */}
```ts
class UserProfile extends CoMap {
name = co.string;
avatar = co.ref(FileStream);
}
```
See the corresponding sections for [creating](/docs/using-covalues/creation#filestream-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading),
[reading from](/docs/using-covalues/reading#filestream-reading) and
[writing to](/docs/using-covalues/writing#filestream-writing) FileStreams.
### `SchemaUnion` (declaration)
SchemaUnion is a helper type that allows you to load and refer to multiple subclasses of a CoMap schema, distinguished by a discriminating field.
You declare them with a base class type and discriminating lambda, in which you have access to the `RawCoMap`, on which you can call `get` with the field name to get the discriminating value.
{/* prettier-ignore */}
```ts
class BaseWidget extends CoMap {
type = co.string;
}
class ButtonWidget extends BaseWidget {
type = co.literal("button");
label = co.string;
}
class SliderWidget extends BaseWidget {
type = co.literal("slider");
min = co.number;
max = co.number;
}
const WidgetUnion = SchemaUnion.Of((raw) => {
switch (raw.get("type")) {
case "button": return ButtonWidget;
case "slider": return SliderWidget;
default: throw new Error("Unknown widget type");
}
});
```
See the corresponding sections for [creating](/docs/using-covalues/creation#schemaunion-creation),
[subscribing/loading](/docs/using-covalues/subscription-and-loading) and
[narrowing](/docs/using-covalues/reading#schemaunion-narrowing) SchemaUnions.
## CoValue field/item types
Now that we've seen the different types of CoValues, let's see more precisely how we declare the fields or items they contain.
### Primitive fields
You can declare primitive field types using the `co` declarer:
{/* prettier-ignore */}
```ts
export class Person extends CoMap {
title = co.string;
}
export class ListOfColors extends CoList.Of(co.string) {}
```
Here's a quick overview of the primitive types you can use:
{/* prettier-ignore */}
```ts
co.string;
co.number;
co.boolean;
co.null;
co.Date;
co.literal("waiting", "ready");
```
Finally, for more complex JSON data, that you *don't want to be collaborative internally* (but only ever update as a whole), you can use `co.json()`:
{/* prettier-ignore */}
```ts
co.json<{ name: string }>();
```
For more detail, see the API Reference for the [`co` field declarer](/api-reference/jazz-tools#co).
### Refs to other CoValues
To represent complex structured data with Jazz, you form trees or graphs of CoValues that reference each other.
Internally, this is represented by storing the IDs of the referenced CoValues in the corresponding fields, but Jazz abstracts this away, making it look like nested CoValues you can get or assign/insert.
The important caveat here is that **a referenced CoValue might or might not be loaded yet,** but we'll see what exactly that means in [Subscribing and Deep Loading](/docs/using-covalues/subscription-and-loading).
In Schemas, you declare Refs using the `co.ref()` declarer:
{/* prettier-ignore */}
```ts
class Company extends CoMap {
members = co.ref(ListOfPeople);
}
class ListOfPeople extends CoList.Of(co.ref(Person)) {}
```
#### Optional Refs
β οΈ If you want to make a referenced CoValue field optional, you *have to* use `co.optional.ref()`: β οΈ
{/* prettier-ignore */}
```ts
class Person extends CoMap {
pet = co.optional.ref(Pet);
}
```
### Computed fields & methods
Since CoValue schemas are based on classes, you can easily add computed fields and methods:
{/* prettier-ignore */}
```ts
class Person extends CoMap {
firstName = co.string;
lastName = co.string;
dateOfBirth = co.Date;
get name() {
return `${this.firstName} ${this.lastName}`;
}
ageAsOf(date: Date) {
return differenceInYears(date, this.dateOfBirth);
}
}
```
#### Accounts & migrations
# Accounts & Migrations
## CoValues as a graph of data rooted in accounts
Compared to traditional relational databases with tables and foreign keys,
Jazz is more like a graph database, or GraphQL APIs —
where CoValues can arbitrarily refer to each other and you can resolve references without having to do a join.
(See [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
To find all data related to a user, the account acts as a root node from where you can resolve all the data they have access to.
These root references are modeled explicitly in your schema, distinguishing between data that is typically public
(like a user's profile) and data that is private (like their messages).
### `Account.root` - private data a user cares about
Every Jazz app that wants to refer to per-user data needs to define a custom root `CoMap` schema and declare it in a custom `Account` schema as the `root` field:
{/* prettier-ignore */}
```ts
export class MyAppAccount extends Account {
root = co.ref(MyAppRoot);
}
export class MyAppRoot extends CoMap {
myChats = co.ref(ListOfChats);
myContacts = co.ref(ListOfAccounts);
}
```
### `Account.profile` - public data associated with a user
The built-in `Account` schema class comes with a default `profile` field, which is a CoMap (in a Group with `"everyone": "reader"` - so publicly readable permissions)
that is set up for you based on the username the `AuthMethod` provides on account creation.
Their pre-defined schemas roughly look like this:
{/* prettier-ignore */}
```ts
// ...somehwere in jazz-tools itself...
export class Account extends Group {
profile = co.ref(Profile);
}
export class Profile extends CoMap {
name = co.string;
}
```
If you want to keep the default `Profile` schema, but customise your account's private `root`, all you have to do is define a new `root` field in your account schema:
(You don't have to explicitly re-define the `profile` field, but it makes it more readable that the Account contains both `profile` and `root`)
{/* prettier-ignore */}
```ts
export class MyAppAccount extends Account {
profile = co.ref(Profile);
root = co.ref(MyAppRoot);
}
```
If you want to extend the `profile` to contain additional fields (such as an avatar `ImageDefinition`), you can declare your own profile schema class that extends `Profile`:
{/* prettier-ignore */}
```ts
export class MyAppAccount extends Account {
profile = co.ref(MyAppProfile);
root = co.ref(MyAppRoot);// old
}
export class MyAppRoot extends CoMap {// old
myChats = co.ref(ListOfChats);// old
myContacts = co.ref(ListOfAccounts);// old
}// old
export class MyAppProfile extends Profile {
name = co.string; // compatible with default Profile schema
avatar = co.optional.ref(ImageDefinition);
}
```
## Resolving CoValues starting at `profile` or `root`
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a depth-spec (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
{/* prettier-ignore */}
```tsx
function DashboardPageComponent() {
const { me } = useAccount({ profile: {}, root: { myChats: {}, myContacts: {}}});
return
Dashboard
{me ?
Logged in as {me.profile.name}
My chats
{me.root.myChats.map((chat) => )}
My contacts
{me.root.myContacts.map((contact) => )}
: "Loading..."}
}
```
## Populating and evolving `root` and `profile` schemas with migrations
As you develop your app, you'll likely want to
- initialise data in a user's `root` and `profile`
- add more data to your `root` and `profile` schemas
You can achieve both by overriding the `migrate()` method on your `Account` schema class.
### When migrations run
Migrations are run after account creation and every time a user logs in.
Jazz waits for the migration to finish before passing the account to your app's context.
### Initialising user data after account creation
{/* prettier-ignore */}
```ts
export class MyAppAccount extends Account {
root = co.ref(MyAppRoot);
async migrate() {
// we specifically need to check for undefined,
// because the root might simply be not loaded (`null`) yet
if (this.root === undefined) {
this.root = MyAppRoot.create({
// Using a group to set the owner is always a good idea.
// This way if in the future we want to share
// this coValue we can do so easily.
myChats: ListOfChats.create([], Group.create()),
myContacts: ListOfAccounts.create([], Group.create())
});
}
}
}
```
### Adding/changing fields to `root` and `profile`
To add new fields to your `root` or `profile` schemas, amend their corresponding schema classes with new fields,
and then implement a migration that will populate the new fields for existing users (by using initial data, or by using existing data from old fields).
To do deeply nested migrations, you might need to use the asynchronous `ensureLoaded()` method before determining whether the field already exists, or is simply not loaded yet.
Now let's say we want to add a `myBookmarks` field to the `root` schema:
{/* prettier-ignore */}
```ts
export class MyAppAccount extends Account {
root = co.ref(MyAppRoot);// old
async migrate() { // old
if (this.root === undefined) { // old
this.root = MyAppRoot.create({ // old
myChats: ListOfChats.create([], Group.create()), // old
myContacts: ListOfAccounts.create([], Group.create()) // old
}); // old
} // old
// We need to load the root field to check for the myContacts field
const result = await this.ensureLoaded({
root: {},
});
if (!result) throw new Error("Root missing!"); // this should never happen
const { root } = result;
// we specifically need to check for undefined,
// because myBookmarks might simply be not loaded (`null`) yet
if (root.myBookmarks === undefined) {
root.myBookmarks = ListOfBookmarks.create([], Group.create());
}
}
}
```
{/*
TODO: Add best practice: only ever add fields
Note: explain and reassure that there will be more guardrails in the future
https://github.com/garden-co/jazz/issues/1160
*/}
### Groups, permissions & sharing
#### Groups as permission scopes
# Groups as permission scopes
Every CoValue has an owner, which can be a `Group` or an `Account`.
You can use a `Group` to grant access to a CoValue to multiple users. These users can
have different roles, such as "writer", "reader" or "admin".
...more docs coming soon
## Creating a Group
Here's how you can create a `Group`.
```tsx
const group = Group.create({ owner: me });
```
The `Group` itself is a CoValue, and whoever owns it is the initial admin.
You typically add members using [public sharing](/docs/groups/sharing#public-sharing) or [invites](/docs/groups/sharing#invites).
But if you already know their ID, you can add them directly (see below).
## Adding group members by ID
You can add group members by ID by using `Account.load` and `Group.addMember`.
```tsx
const group = Group.create();
const bob = await Account.load(bobsID, []);
group.addMember(bob, "writer");
```
Note: if the account ID is of type `string`, because it comes from a URL parameter or something similar, you need to cast it to `ID` first:
```tsx
const bob = await Account.load(bobsID as ID, []);
group.addMember(bob, "writer");
```
...more docs coming soon
## Getting the Group of an existing CoValue
You can get the group of an existing CoValue by using `coValue._owner`.
```tsx
const group = existingCoValue._owner;
const newValue = MyCoMap.create(
{ color: "red"},
{ owner: group }
);
```
Because `._owner` can be an `Account` or a `Group`, in cases where you specifically need to use `Group` methods (such as for adding members or getting your own role), you can cast it to assert it to be a Group:
```tsx
const group = existingCoValue._owner.castAs(Group);
group.addMember(bob, "writer");
group.myRole();
```
...more docs coming soon
#### Public sharing & invites
# Public sharing and invites
...more docs coming soon
## Public sharing
You can share CoValues publicly by setting the `owner` to a `Group`, and granting
access to "everyone".
```ts
const group = Group.create();
group.addMember("everyone", "writer"); // *highlight*
```
This is done in the [chat example](https://github.com/garden-co/jazz/tree/main/examples/chat) where anyone can join the chat, and send messages.
You can also [add members by Account ID](/docs/groups/intro#adding-group-members-by-id).
## Invites
You can grant users access to a CoValue by sending them an invite link.
This is used in the [pet example](https://github.com/garden-co/jazz/tree/main/examples/pets)
and the [todo example](https://github.com/garden-co/jazz/tree/main/examples/todo).
```ts
createInviteLink(organization, "writer"); // or reader, or admin
```
```ts
createInviteLink(organization, "writer"); // or reader, or admin
```
```ts
createInviteLink(organization, "writer"); // or reader, or admin
```
```ts
createInviteLink(organization, "writer"); // or reader, or admin
```
It generates a URL that looks like `.../invite/[CoValue ID]/[inviteSecret]`
In your app, you need to handle this route, and let the user accept the invitation,
as done [here](https://github.com/garden-co/jazz/tree/main/examples/pets/src/2_main.tsx).
```ts
useAcceptInvite({
invitedObjectSchema: PetPost,
onAccept: (petPostID) => navigate("/pet/" + petPostID),
});
```
### Authentication
#### Overview
# Authentication methods
Jazz supports a variety of authentication methods, which you can use to authenticate users in your app.
- Passphrase (built-in)
- Passkey (built-in)
- Clerk ([React](https://www.npmjs.com/package/jazz-react-auth-clerk) and [vanilla](https://www.npmjs.com/package/jazz-browser-auth-clerk) packages)
## Passphrase
Passphrase authentication allows users to create a new account or log in with an existing one by providing a passphrase.
Passphrase authentication is supported out of the box and imported from `jazz-react`.
### How to use
1. Setup up Jazz as described in the [setup guide](/docs/project-setup).
2. Use the `usePassphraseAuth` hook to authenticate.
```ts
// ...
const [passphraseAuth, passphraseState] = usePassphraseAuth({ appName });
```
## Passkey
Passkey authentication allows users to create a new account or log in with an existing one by providing a passkey.
Passkey authentication is supported out of the box.
We have a [minimal example of a passkey authentication setup](https://github.com/garden-co/jazz/tree/main/examples/passkey).
### How to use
1. Setup up Jazz as described in the [setup guide](/docs/project-setup).
2. Use the `usePasskeyAuth` hook to authenticate.
```ts
// ...
const [passkeyAuth, passkeyState] = usePasskeyAuth({ appName });
```
## Clerk
We have a React package `jazz-react-auth-clerk` to add Clerk authentication to your app.
We have a [minimal example of a Clerk authentication setup](https://github.com/garden-co/jazz/tree/main/examples/clerk).
### How to use
1. Setup up Jazz as described in the [setup guide](/docs/project-setup).
2. Install Clerk as described in the [Clerk docs](https://clerk.com/docs/components/overview).
3. Then add the appropriate package to your project (e.g. `jazz-react-auth-clerk`).
```bash
pnpm install jazz-react-auth-clerk
```
4. Provide a Clerk instance to the `useJazzClerkAuth` hook.
```tsx
// ...
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
const [auth, state] = useJazzClerkAuth(clerk);
return (
<>
{clerk.user && auth ? (
) : (
)}
>
);
}
createRoot(document.getElementById("root")!).render(
,
);
```
### Design patterns
#### Form
# Creating and updating CoValues in a form
Normally, we implement forms using
[the onSubmit handler](https://react.dev/reference/react-dom/components/form#handle-form-submission-on-the-client),
or by making [a controlled form with useState](https://christinakozanian.medium.com/building-controlled-forms-with-usestate-in-react-f9053ad255a0),
or by using special libraries like [react-hook-form](https://www.react-hook-form.com).
In Jazz, we can do something simpler and more powerful, because CoValues give us reactive,
persisted state which we can use to directly edit live objects, and represent auto-saved drafts.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
## Updating a CoValue
To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server, so
we don't need to handle form submissions either.
```tsx
order.name = e.target.value}
/>
```
This means we can write update forms in fewer lines of code.
## Creating a CoValue
However, when creating a CoValue, the CoValue does not exist yet, so we don't have the advantages previously mentioned.
There's a way around this, and it provides unexpected benefits too.
### Using a Draft CoValue
Let's say we have a CoValue called `BubbleTeaOrder`. We can create a "draft" CoValue,
which is an empty version of a `BubbleTeaOrder`, that we can then modify when we are "creating"
a new CoValue.
A `DraftBubbleTeaOrder` is essentially a copy of `BubbleTeaOrder`, but with all the fields made optional.
```tsx
// schema.ts
export class BubbleTeaOrder extends CoMap {
name = co.string;
}
export class DraftBubbleTeaOrder extends CoMap {
name = co.optional.string;
}
```
## Writing the components in React
Let's write the form component that will be used for both create and update.
```tsx
// OrderForm.tsx
export function OrderForm({
order,
onSave,
}: {
order: BubbleTeaOrder | DraftBubbleTeaOrder;
onSave?: (e: React.FormEvent) => void;
}) {
return (
);
}
```
### Writing the edit form
To make the edit form, simply pass the `BubbleTeaOrder`.
```tsx
// EditOrder.tsx
export function EditOrder(props: { id: ID }) {
const order = useCoState(BubbleTeaOrder, props.id, []);
if (!order) return;
return ;
}
```
### Writing the create form
For the create form, we need to:
1. Create a draft order.
2. Edit the draft order.
3. Convert the draft order to a "real" order on submit.
Here's how that looks like:
```tsx
// CreateOrder.tsx
export function CreateOrder() {
const { me } = useAccount();
const [draft, setDraft] = useState();
useEffect(() => {
setDraft(DraftBubbleTeaOrder.create({}));
}, [me?.id]);
const onSave = (e: React.FormEvent) => {
e.preventDefault();
if (!draft) return;
const order = draft as BubbleTeaOrder;
console.log("Order created:", order);
};
if (!draft) return;
return ;
}
```
## Validation
In a `BubbleTeaOrder`, the `name` field is required, so it would be a good idea to validate this before turning the draft into a real order.
Update the schema to include a `validate` method.
```ts
// schema.ts
export class DraftBubbleTeaOrder extends CoMap { // old
name = co.optional.string; // old
validate() {
const errors: string[] = [];
if (!this.name) {
errors.push("Please enter a name.");
}
return { errors };
}
} // old
```
Then perform the validation on submit.
```tsx
// CreateOrder.tsx
export function CreateOrder() { // old
const { me } = useAccount(); // old
const [draft, setDraft] = useState(); // old
useEffect(() => { // old
setDraft(DraftBubbleTeaOrder.create({})); // old
}, [me?.id]); // old
const onSave = (e: React.FormEvent) => { // old
e.preventDefault(); // old
if (!draft) return; // old
const validation = draft.validate();
if (validation.errors.length > 0) {
console.log(validation.errors);
return;
}
const order = draft as BubbleTeaOrder; // old
console.log("Order created:", order); // old
}; // old
if (!draft) return; // old
return ; // old
} // old
```
## Saving the user's work-in-progress
It turns out that using this pattern also provides a UX improvement.
By storing the draft in the user's account, they can come back to it anytime without losing their work. π
```ts
// schema.ts
export class BubbleTeaOrder extends CoMap { // old
name = co.string; // old
} // old
export class DraftBubbleTeaOrder extends CoMap { // old
name = co.optional.string; // old
} // old
export class AccountRoot extends CoMap {
draft = co.ref(DraftBubbleTeaOrder);
}
export class JazzAccount extends Account {
root = co.ref(AccountRoot);
migrate(this: JazzAccount, creationProps?: { name: string }) {
if (this.root === undefined) {
const draft = DraftBubbleTeaOrder.create({});
this.root = AccountRoot.create({ draft });
}
}
}
```
Let's not forget to update the `AccountSchema`.
```ts
function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
return ( // old
<> // old
// old
{children} // old
// old
// old
> // old
); // old
} // old
// Register the Account schema so `useAccount` returns our custom `JazzAccount`
declare module "jazz-react" {
interface Register {
Account: JazzAccount;
}
}
```
Instead of creating a new draft every time we use the create form, let's use the draft from the account root.
```tsx
// CreateOrder.tsx
export function CreateOrder() {// old
const { me } = useAccount({ root: { draft: {} } });
if (!me?.root) return;
const onSave = (e: React.FormEvent) => {// old
e.preventDefault();// old
const draft = me.root.draft;
if (!draft) return;
const validation = draft.validate();// old
if (validation.errors.length > 0) {// old
console.log(validation.errors);// old
return;// old
}// old
const order = draft as BubbleTeaOrder;// old
console.log("Order created:", order);// old
// create a new empty draft
me.root.draft = DraftBubbleTeaOrder.create(
{},
);
};// old
return
} // old
function CreateOrderForm({
id,
onSave,
}: {
id: ID;
onSave: (e: React.FormEvent) => void;
}) {
const draft = useCoState(DraftBubbleTeaOrder, id);
if (!draft) return;
return ;
}
```
When the new draft is created, we need to call `useCoState` again, so that we are passing the new draft to ``.
There you have it! Notice that when you refresh the page, you will see your unsaved changes.
## Draft indicator
To improve the UX even further, in just a few more steps, we can tell the user that they currently have unsaved changes.
Simply add a `hasChanges` checker to your schema.
```ts
// schema.ts
export class DraftBubbleTeaOrder extends CoMap { // old
name = co.optional.string; // old
validate() { // old
const errors: string[] = []; // old
if (!this.name) { // old
errors.push("Plese enter a name."); // old
} // old
return { errors }; // old
} // old
get hasChanges() {
return Object.keys(this._edits).length;
}
} // old
```
In the UI, you can choose how you want to show the draft indicator.
```tsx
// DraftIndicator.tsx
export function DraftIndicator() {
const { me } = useAccount({
root: { draft: {} },
});
if (me?.root.draft?.hasChanges) {
return (
You have a draft
);
}
}
```
A more subtle way is to show a small dot next to the Create button.
## Handling different types of data
Forms can be more complex than just a single string field, so we've put together an example app that shows you
how to handle single-select, multi-select, date, and boolean inputs.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/form)
```tsx
export class BubbleTeaOrder extends CoMap {
baseTea = co.literal(...BubbleTeaBaseTeaTypes);
addOns = co.ref(ListOfBubbleTeaAddOns);
deliveryDate = co.Date;
withMilk = co.boolean;
instructions = co.optional.string;
}
```
#### Organization/Team
# Sharing data through Organizations
Organizations are a way to share a set of data between users.
Different apps have different names for this concept, such as "teams" or "workspaces".
We'll use the term Organization.
[See the full example here.](https://github.com/garden-co/jazz/tree/main/examples/organization)
## Defining the schema for an Organization
Create a CoMap shared by the users of the same organization to act as a root (or "main database") for the shared data within an organization.
For this example, users within an `Organization` will be sharing `Project`s.
```ts
// schema.ts
export class Project extends CoMap {
name = co.string;
}
export class ListOfProjects extends CoList.Of(co.ref(Project)) {}
export class Organization extends CoMap {
name = co.string;
// shared data between users of each organization
projects = co.ref(ListOfProjects);
}
export class ListOfOrganizations extends CoList.Of(co.ref(Organization)) {}
```
Learn more about [defining schemas](/docs/schemas/covalues).
## Adding a list of Organizations to the user's Account
Let's add the list of `Organization`s to the user's Account `root` so they can access them.
```ts
// schema.ts
export class JazzAccountRoot extends CoMap {
organizations = co.ref(ListOfOrganizations);
}
export class JazzAccount extends Account {
root = co.ref(JazzAccountRoot);
async migrate() {
if (this.root === undefined) {
// Using a Group as an owner allows you to give access to other users
const organizationGroup = Group.create();
const organizations = ListOfOrganizations.create(
[
// Create the first Organization so users can start right away
Organization.create(
{
name: "My organization",
projects: ListOfProjects.create([], organizationGroup),
},
organizationGroup,
),
],
);
this.root = JazzAccountRoot.create(
{ organizations },
);
}
}
}
```
This schema now allows users to create `Organization`s and add `Project`s to them.
[See the schema for the example app here.](https://github.com/garden-co/jazz/blob/main/examples/organization/src/schema.ts)
## Adding other users to an Organization
To give users access to an `Organization`, you can either send them an invite link, or
add their `Account` manually.
### Adding users through invite links
Here's how you can generate an [invite link](/docs/groups/sharing#invites).
When the user accepts the invite, add the `Organization` to the user's `organizations` list.
```ts
const onAccept = async (organizationId: ID) => {
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
organizations: [],
},
});
if (!me) throw new Error("Failed to load account data");
const organization = await Organization.load(organizationId, []);
if (!organization) throw new Error("Failed to load organization data");
const ids = me.root.organizations.map(
(organization) => organization?.id,
);
if (ids.includes(organizationId)) return;
me.root.organizations.push(organization);
navigate("/organizations/" + organizationId);
};
useAcceptInvite({
invitedObjectSchema: Organization,
onAccept,
});
```
### Adding users through their Account ID
...more on this coming soon
## API Reference
## Resources
- [Documentation](https://jazz.tools/docs): Detailed documentation about Jazz
- [Examples](https://jazz.tools/examples): Code examples and tutorials