# Jazz
## Documentation
### Getting started
#### Introduction
### ai-tools Implementation
# Using AI to build Jazz apps
AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at.
However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong.
To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor.
llms-full.txt
## Setting up AI tools
Every tool is different, but generally, you'll need to either paste the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file directly in your prompt, or attach the file to the tool.
### ChatGPT and v0
Upload the txt file in your prompt.

### Cursor
1. Go to Settings > Cursor Settings > Features > Docs
2. Click "Add new doc"
3. Enter the following URL:
```
https://jazz.tools/llms-full.txt
```
## llms.txt convention
We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing.
## Limitations and considerations
AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good).
We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on [Discord](https://discord.gg/utDMjHYg42).
---
### inspector Implementation
# Jazz Inspector
[Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues.
For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app.
[https://inspector.jazz.tools](https://inspector.jazz.tools)
## Exporting current account to Inspector from your app
In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`.
## Embedding the Inspector widget into your app
Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window.
Install the package.
```sh
npm install jazz-inspector
```
Render the component within your `JazzProvider`.
```sh
// old
// old
```
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example.
---
### sync-and-storage Implementation
# Sync and storage
## Using Jazz Cloud
Simply use `wss://cloud.jazz.tools/?key=...` as the sync server URL.
Jazz Cloud will
- sync CoValues in real-time between users and devices
- safely persist CoValues on redundant storage nodes with additional backups
- make use of geographically distributed cache nodes for low latency
### Free public alpha
- Jazz Cloud is free during the public alpha, with no strict usage limits
- We plan to keep a free tier, so you'll always be able to get started with zero setup
- See [Jazz Cloud pricing](/cloud#pricing) for more details
- β οΈ Please use a valid email address as your API key.
Your full sync server URL should look something like
```wss://cloud.jazz.tools/?key=you@example.com```
Once we support per-app API keys, we'll email you an API key you can use instead.
## Running your own sync server
You can run your own sync server using:
```sh
npx jazz-run sync
```
And then use `ws://localhost:4200` as the sync server URL.
You can also run this simple sync server behind a proxy that supports WebSockets, for example to provide TLS.
In this case, provide the WebSocket endpoint your proxy exposes as the sync server URL.
### Command line options:
- `--port` / `-p` - the port to run the sync server on. Defaults to 4200.
- `--in-memory` - keep CoValues in-memory only and do sync only, no persistence. Persistence is enabled by default.
- `--db` - the path to the file where to store the data (SQLite). Defaults to `sync-db/storage.db`.
### Source code
The implementation of this simple sync server is available open-source [on GitHub](https://github.com/garden-co/jazz/blob/main/packages/jazz-run/src/startSyncServer.ts).
#### 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 } from "jazz-react"; // 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
#### AI tools
# Using AI to build Jazz apps
AI tools, particularly large language models (LLMs), can accelerate your development with Jazz. Searching docs, responding to questions and even helping you write code are all things that LLMs are starting to get good at.
However, Jazz is a rapidly evolving framework, so sometimes AI might get things a little wrong.
To help the LLMs, we provide the Jazz documentation in a txt file that is optimized for use with AI tools, like Cursor.
llms-full.txt
## Setting up AI tools
Every tool is different, but generally, you'll need to either paste the contents of the [llms-full.txt](https://jazz.tools/llms-full.txt) file directly in your prompt, or attach the file to the tool.
### ChatGPT and v0
Upload the txt file in your prompt.

### Cursor
1. Go to Settings > Cursor Settings > Features > Docs
2. Click "Add new doc"
3. Enter the following URL:
```
https://jazz.tools/llms-full.txt
```
## llms.txt convention
We follow the llms.txt [proposed standard](https://llmstxt.org/) for providing documentation to AI tools at inference time that helps them understand the context of the code you're writing.
## Limitations and considerations
AI is amazing, but it's not perfect. What works well this week could break next week (or be twice as good).
We're keen to keep up with changes in tooling to help support you building the best apps, but if you need help from humans (or you have issues getting set up), please let us know on [Discord](https://discord.gg/utDMjHYg42).
#### Inspector
# Jazz Inspector
[Jazz Inspector](https://inspector.jazz.tools) is a tool to visually inspect a Jazz account or other CoValues.
For now, you can get your account credentials from the `jazz-logged-in-secret` local storage key from within your Jazz app.
[https://inspector.jazz.tools](https://inspector.jazz.tools)
## Exporting current account to Inspector from your app
In development mode, you can launch the Inspector from your Jazz app to inspect your account by pressing `Cmd+J`.
## Embedding the Inspector widget into your app
Alternatively, you can embed the Inspector directly into your app, so you don't need to open a separate window.
Install the package.
```sh
npm install jazz-inspector
```
Render the component within your `JazzProvider`.
```sh
// old
// old
```
Check out the [music player app](https://github.com/garden-co/jazz/blob/main/examples/music-player/src/2_main.tsx) for a full example.
### 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
npm i -S @azure/core-asynciterator-polyfill react-native-url-polyfill readable-stream react-native-get-random-values @craftzdog/react-native-buffer @op-engineering/op-sqlite
npm i -S jazz-tools jazz-react-native jazz-react-native-media-images
```
> note: Hermes has added support for `atob` and `btoa` in React Native 0.74. If you are using earlier versions, you may also need to polyfill `atob` and `btoa` in your `package.json` . Packages to try include `text-encoding` and `base-64`, and you can drop `@bacons/text-decoder`.
### 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.sourceExts = ["mjs", "js", "json", "ts", "tsx"];
config.resolver.requireCycleIgnorePatterns = [/(^|\/|\\)node_modules($|\/|\\)/];
module.exports = config;
```
#### 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.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:
```js
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 } from "jazz-react-native";
import { MyAppAccount } from "./schema";
export function MyJazzProvider({ children }: { children: React.ReactNode }) {
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 few custom attributes to ``:
- `kvStore`
- `ExpoSecureStoreAdapter` (default)
- example: `MMKVStore` - roll your own, using MMKV
- `AccountSchema`
- `Account` (default)
- `CryptoProvider`
- `PureJSCrypto` (default)
- `RNQuickCrypto` - C++ accelerated crypto provider
### 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
Wrap your application with ``, this is where you specify the sync & storage server to connect to (see [Sync and storage](/docs/react/sync-and-storage)).
{/* prettier-ignore */}
```tsx
ReactDOM.createRoot(document.getElementById("root")!).render( // old
);// old
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
declare module "jazz-react" {
interface Register {
Account: MyAppAccount;
}
}
```
## 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 { MyAppAccount } from "./schema"; // old
export function MyJazzProvider(props: { children: React.ReactNode }) {
return (
{props.children}
);
}
```
- 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() {
return () => [
h(
JazzProvider,
{
AccountSchema: ToDoAccount,
auth: authMethod.value,
peer: "wss://cloud.jazz.tools/?key=vue-todo-example-jazz@garden.co",
},
{
default: () => h(App),
},
),
];
},
});
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();
});
```
---
### vue Implementation
# Upgrade to Jazz 0.9.0
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:
{/* prettier-ignore */}
```typescript
Hello {{ me.profile?.name }}
```
## New testing utilities
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: {},
});
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),
});
```
#### Group inheritance
# Group Inheritance
Groups can inherit members from other groups using the `extend` method.
When a group extends another group, members of the parent group will become automatically part of the child group.
## Basic Usage
Here's how to extend a group:
```typescript
const playlistGroup = Group.create();
const trackGroup = Group.create();
// This way track becomes visible to the members of playlist
trackGroup.extend(playlistGroup);
```
When you extend a group:
- Members of the parent group get access to the child group
- Their roles are inherited (with some exceptions, see [below](#role-inheritance-rules))
- Removing a member from the parent group also removes their access to child groups
## Inheriting members but overriding their role
In some cases you might want to inherit all members from a parent group but override/flatten their roles to the same specific role in the child group. You can do so by passing an "override role" as a second argument to `extend`:
```typescript
const organizationGroup = Group.create();
organizationGroup.addMember(bob, "admin");
const billingGroup = Group.create();
// This way the members of the organization can only read the billing data
billingGroup.extend(organizationGroup, "reader");
```
The "override role" works in both directions:
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
parentGroup.addMember(alice, "admin");
const childGroup = Group.create();
childGroup.extend(parentGroup, "writer");
// Bob and Alice are now writers in the child group
```
## Multiple Levels of Inheritance
Groups can be extended multiple levels deep:
```typescript
const grandParentGroup = Group.create();
const parentGroup = Group.create();
const childGroup = Group.create();
childGroup.extend(parentGroup);
parentGroup.extend(grandParentGroup);
```
Members of the grandparent group will get access to all descendant groups based on their roles.
## Permission Changes
When you remove a member from a parent group, they automatically lose access to all child groups. We handle key rotation automatically to ensure security.
```typescript
// Remove member from parent
await parentGroup.removeMember(bob);
// Bob loses access to both parent and child groups
```
## Role Inheritance Rules
If the account is already a member of the child group, it will get the more permissive role:
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "reader");
const childGroup = Group.create();
parentGroup.addMember(bob, "writer");
childGroup.extend(parentGroup);
// Bob stays a writer because his role is higher
// than the inherited reader role.
```
When extending groups, only admin, writer and reader roles are inherited:
```typescript
const parentGroup = Group.create();
parentGroup.addMember(bob, "writeOnly");
const childGroup = Group.create();
childGroup.extend(parentGroup);
// Bob does not become a member of the child group
```
To extend a group:
1. The current account must be an admin in the child group
2. The current account must be a member of the parent group
```typescript
const companyGroup = company._owner.castAs(Group)
const teamGroup = Group.create();
// Works only if I'm a member of companyGroup
teamGroup.extend(companyGroup);
```
## Example: Team Hierarchy
Here's a practical example of using group inheritance for team permissions:
```typescript
// Company-wide group
const companyGroup = Group.create();
companyGroup.addMember(CEO, "admin");
// Team group with elevated permissions
const teamGroup = Group.create();
teamGroup.extend(companyGroup); // Inherits company-wide access
teamGroup.addMember(teamLead, "admin");
teamGroup.addMember(developer, "writer");
// Project group with specific permissions
const projectGroup = Group.create();
projectGroup.extend(teamGroup); // Inherits team permissions
projectGroup.addMember(client, "reader"); // Client can only read project items
```
This creates a hierarchy where:
- The CEO has admin access to everything
- Team members get writer access to team and project content
- Team leads get admin access to team and project content
- The client can only read project content
### Authentication
#### Overview
# Authentication in Jazz
Jazz authentication is based on cryptographic keys ("Account keys"). Their public part represents a user's identity, their secret part lets you act as that user.
When a user loads a Jazz application for the first time, we create a new Account by generating keys and storing them locally.
Without any additional steps the user can use Jazz normally, but they would be limited to use on only one device.
To make Accounts work across devices, you can store/retrieve the account keys from an authentication method by using the corresponding hooks and providers.
## Authentication with passkeys
Passkey authentication is fully local-first and the most secure of the auth methods that Jazz provides (because keys are managed by the device/operating system itself).
It is based on the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) and is both very easy to use (using familiar FaceID/TouchID flows) and widely supported.
Using passkeys in Jazz is as easy as this:
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [username, setUsername] = useState("");
const auth = usePasskeyAuth({ // Must be inside the JazzProvider!
appName: "My super-cool web app",
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return
);
}
```
You can try our passkey authentication using our [passkey example](https://passkey-demo.jazz.tools/) or the [music player demo](https://music-demo.jazz.tools/).
## Passphrase-based authentication
Passphrase authentication lets users log into any device using a Bitcoin-style passphrase. This means users are themselves responsible for storing the passphrase safely.
The passphrase is generated from the local account certificate using a wordlist of your choice.
You can find a set of ready-to-use wordlists in the [bip39](https://github.com/bitcoinjs/bip39/tree/a7ecbfe2e60d0214ce17163d610cad9f7b23140c/src/wordlists) repository.
For example:
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({ // Must be inside the JazzProvider!
wordlist: englishWordlist,
});
if (auth.state === "signedIn") { // You can also use `useIsAuthenticated()`
return
);
}
```
For example:
{/* prettier-ignore */}
```tsx
export function AuthModal({ open, onOpenChange }: AuthModalProps) {
const [loginPassphrase, setLoginPassphrase] = useState("");
const auth = usePassphraseAuth({
wordlist: englishWordlist,
});
if (auth.state === "signedIn") {
return You are already signed in;
}
const handleSignUp = async () => {
await auth.signUp();
onOpenChange(false);
};
const handleLogIn = async () => {
await auth.logIn(loginPassphrase);
onOpenChange(false);
};
return (
Your current passphraseLog in with your passphrase
);
}
```
You can try our passphrase authentication using our [passphrase example](https://passphrase-demo.jazz.tools/) or the [todo list demo](https://todo-demo.jazz.tools/).
## Integration with Clerk
Jazz can be used with [Clerk](https://clerk.com/) to authenticate users.
This authentication method is not fully local-first, because the login and signup need to be done while online. Clerk and anyone who is an admin in the app's Clerk org are trusted with the user's key secret and could impersonate them.
However, once authenticated, your users won't need to interact with Clerk anymore, and are able to use all of Jazz's features without needing to be online.
The clerk provider is not built into `jazz-react` and needs the `jazz-react-auth-clerk` package to be installed.
The clerk provider is not built into `jazz-react-native` and needs the `jazz-react-native-auth-clerk` package to be installed.
After installing the package you can use the `JazzProviderWithClerk` component to wrap your app:
```tsx
function JazzProvider({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
{children}
);
}
createRoot(document.getElementById("root")!).render(
);
```
```tsx
function JazzAndAuth({ children }: { children: React.ReactNode }) {
const clerk = useClerk();
return (
{children}
);
}
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
if (!publishableKey) {
throw new Error(
"Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env",
);
}
return (
);
}
```
Then you can use the [Clerk auth methods](https://clerk.com/docs/references/react/overview) to log in and sign up:
{/* prettier-ignore */}
```tsx
export function AuthButton() {
const { logOut } = useAccount();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return ;
}
return ;
}
```
Then you can use the [Clerk auth methods](https://clerk.com/docs/references/expo/overview) to log in and sign up:
{/* prettier-ignore */}
```tsx
export function AuthButton() {
const { logOut } = useAccount();
const { signIn, setActive, isLoaded } = useSignIn();
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return ;
}
// Login code with Clerk Expo
}
```
## Migrating data from anonymous to authenticated account
You may want allow your users to use your app without authenticating (a poll response for example). When *signing up* their anonymous account is transparently upgraded using the provided auth method, keeping the data stored in the account intact.
However, a user may realise that they already have an existing account *after using the app anonymously and having already stored data in the anonymous account*.
When they now *log in*, by default the anonymous account will be discarded and this could lead to unexpected data loss.
To avoid this situation we provide the `onAnonymousAccountDiscarded` handler to migrate the data from the anonymous account to the existing authenticated one.
This is an example from our [music player](https://github.com/garden-co/jazz/tree/main/examples/music-player):
```ts
export async function onAnonymousAccountDiscarded(
anonymousAccount: MusicaAccount,
) {
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
root: {
rootPlaylist: {
tracks: [{}],
},
},
});
const me = await MusicaAccount.getMe().ensureLoaded({
root: {
rootPlaylist: {
tracks: [],
},
},
});
for (const track of anonymousAccountRoot.rootPlaylist.tracks) {
if (track.isExampleTrack) continue;
const trackGroup = track._owner.castAs(Group);
trackGroup.addMember(me, "admin");
me.root.rootPlaylist.tracks.push(track);
}
}
```
To see how this works in reality we suggest you to try
to upload a song in the [music player demo](https://music-demo.jazz.tools/) and then
try to log in with an existing account.
## Disable network sync for anonymous users
You can disable network sync to make your app local-only under specific circumstances.
For example, you may want to give the opportunity to non-authenticated users to try your app locally-only (incurring no sync traffic), then enable the network sync only when the user is authenticated:
```tsx
```
For more complex behaviours, you can manually control sync by statefully switching when between `"always"` and `"never"`.
### 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
export function MyJazzProvider({ children }: { children: React.ReactNode }) { // old
return ( // old
// old
{children} // 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: [],
},
});
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