# Jazz > **Jazz is a new kind of database** that's **distributed** across your frontend, containers, serverless functions and its own storage cloud. --- # Source: https://jazz.tools/vanilla/llms-full.txt # Source: https://jazz.tools/react-native-expo/llms-full.txt # Source: https://jazz.tools/react-native/llms-full.txt # Source: https://jazz.tools/svelte/llms-full.txt # Source: https://jazz.tools/react/llms-full.txt # Source: https://jazz.tools/vanilla/llms-full.txt # Source: https://jazz.tools/react-native-expo/llms-full.txt # Source: https://jazz.tools/react-native/llms-full.txt # Source: https://jazz.tools/svelte/llms-full.txt # Source: https://jazz.tools/react/llms-full.txt # Jazz (react) ## Getting started ### Overview # Learn some Jazz **Jazz is a new kind of database** that's **distributed** across your frontend, containers, serverless functions and its own storage cloud. It syncs structured data, files and LLM streams instantly, and looks like local reactive JSON state. It also provides auth, orgs & teams, real-time multiplayer, edit histories, permissions, E2E encryption and offline-support out of the box. --- ## Quickstart ### Show me [Check out our tiny To Do list example](/docs#a-minimal-jazz-app) to see what Jazz can do in a nutshell. ### Help me understand Follow our [quickstart guide](/docs/quickstart) for a more detailed guide on building a simple app with Jazz. ### Just want to get started? You can use [create-jazz-app](/docs/tooling-and-resources/create-jazz-app) to create a new Jazz project from one of our starter templates or example apps: ```sh npx create-jazz-app@latest --api-key you@example.com ``` **Using an LLM?** [Add our llms.txt](/react/llms-full.txt) to your context window! **Info:** Requires at least Node.js v20\. See our [Troubleshooting Guide](/docs/troubleshooting) for quick fixes. ## How it works 1. **Define your data** with CoValues schemas 2. **Connect to storage infrastructure** (Jazz Cloud or self-hosted) 3. **Create and edit CoValues** locally 4. **Get automatic sync and persistence** across all devices and users Your UI updates instantly on every change, everywhere. It's like having reactive local state that happens to be shared with the world. ## A Minimal Jazz App Here, we'll scratch the surface of what you can do with Jazz. We'll build a quick and easy To Do list app — easy to use, easy to build, and easy to make comparisons with! This is the end result: we're showing it here running in two iframes, updating in real-time through the Jazz Cloud. Try adding items on the left and watch them appear instantly on the right! **Info: Using Jazz Cloud** These two iframes are syncing through the Jazz Cloud. You can use the toggle in the top right to switch between 'online' and 'offline' on each client, and see how with Jazz, you can keep working even when you're offline. ### Imports Start by importing Jazz into your app. ```ts import { co, z } from 'jazz-tools'; import { JazzBrowserContextManager } from 'jazz-tools/browser'; ``` ### Schema Then, define what your data looks like using [Collaborative Values](/docs/core-concepts/covalues/overview) — the building blocks that make Jazz apps work. ```ts const ToDo = co.map({ title: z.string(), completed: z.boolean() }); const ToDoList = co.list(ToDo); ``` ### Context Next, [give your app some context](/docs/project-setup#give-your-app-context) and tell Jazz your sync strategy — use the Jazz Cloud to get started quickly. We'll also create our to do list and get its ID here to use later. ```ts await new JazzBrowserContextManager().createContext({ sync: { peer: 'wss://cloud.jazz.tools?key=minimal-vanilla-example', when: 'always', }, }); const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]); const listId = newList.$jazz.id; ``` ### Build your UI Now, build a basic UI skeleton for your app. ```ts const app = document.querySelector('#app')!; const id = Object.assign(document.createElement('small'), { innerText: `List ID: ${listId}`, }); const listContainer = document.createElement('div'); app.append(listContainer, id); ``` ### Display Items Display your items and add logic to mark them as done... ```ts function toDoItemElement(todo: co.loaded) { const label = document.createElement('label'); const checkbox = Object.assign(document.createElement('input'), { type: 'checkbox', checked: todo.completed, onclick: () => todo.$jazz.set('completed', checkbox.checked), }); label.append(checkbox, todo.title); return label; } ``` ### Add New Items ...and add new items to the list using an input and a button. ```ts function newToDoFormElement(list: co.loaded) { const form = Object.assign(document.createElement('form'), { onsubmit: (e: Event) => { e.preventDefault(); list.$jazz.push({ title: input.value, completed: false }); } }); const input = Object.assign(document.createElement('input'), { placeholder: 'New task', }); const btn = Object.assign(document.createElement('button'), { innerText: 'Add', }); form.append(input, btn); return form; } ``` ### Subscribe to Changes Now for the magic: listen to changes coming from [**anyone, anywhere**](/docs/permissions-and-sharing/overview), and update your UI in real time. ```ts const unsubscribe = ToDoList.subscribe( listId, { resolve: { $each: true } }, (toDoList) => { const addForm = newToDoFormElement(toDoList); listContainer.replaceChildren( ...toDoList.map((todo) => { return toDoItemElement(todo); }), addForm ); } ); ``` ### Simple Routing Lastly, we'll add a tiny bit of routing logic to be able to share the list by URL: if there's an `id` search parameter, that'll be the list we'll subscribe to later. If we don't have an `id`, we'll [create a new ToDo list](/docs/core-concepts/covalues/colists#creating-colists). We'll replace the section where we created the `ToDoList` above. ```ts //[!code --:2] const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]); const listId = newList.$jazz.id; // [!code ++:8] const listId = new URLSearchParams(window.location.search).get('id'); if (!listId) { const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]); await newList.$jazz.waitForSync(); window.location.search = `?id=${newList.$jazz.id}`; throw new Error('Redirecting...'); } ``` ### All Together Put it all together for a simple Jazz app in less than 100 lines of code. ```ts import { co, z } from 'jazz-tools'; import { JazzBrowserContextManager } from 'jazz-tools/browser'; const ToDo = co.map({ title: z.string(), completed: z.boolean() }); const ToDoList = co.list(ToDo); await new JazzBrowserContextManager().createContext({ sync: { peer: 'wss://cloud.jazz.tools?key=minimal-vanilla-example', when: 'always', }, }); const listId = new URLSearchParams(window.location.search).get('id'); if (!listId) { const newList = ToDoList.create([{ title: 'Learn Jazz', completed: false }]); await newList.$jazz.waitForSync(); window.location.search = `?id=${newList.$jazz.id}`; throw new Error('Redirecting...'); } const app = document.querySelector('#app')!; const id = Object.assign(document.createElement('small'), { innerText: `List ID: ${listId}`, }); const listContainer = document.createElement('div'); app.append(listContainer, id); function toDoItemElement(todo: co.loaded) { const label = document.createElement('label'); const checkbox = Object.assign(document.createElement('input'), { type: 'checkbox', checked: todo.completed, onclick: () => todo.$jazz.set('completed', checkbox.checked), }); label.append(checkbox, todo.title); return label; } function newToDoFormElement(list: co.loaded) { const form = Object.assign(document.createElement('form'), { onsubmit: (e: Event) => { e.preventDefault(); list.$jazz.push({ title: input.value, completed: false }); } }); const input = Object.assign(document.createElement('input'), { placeholder: 'New task', }); const btn = Object.assign(document.createElement('button'), { innerText: 'Add', }); form.append(input, btn); return form; } const unsubscribe = ToDoList.subscribe( listId, { resolve: { $each: true } }, (toDoList) => { const addForm = newToDoFormElement(toDoList); listContainer.replaceChildren( ...toDoList.map((todo) => { return toDoItemElement(todo); }), addForm ); } ); ``` ## Want to see more? Have a look at our [example apps](/examples) for inspiration and to see what's possible with Jazz. From real-time chat and collaborative editors to file sharing and social features — these are just the beginning of what you can build. If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42). We'd love to help you get started. ### Quickstart # Get started with Jazz in 10 minutes This quickstart guide will take you from an empty project to a working app with a simple data model and components to create and display your data. ## Create your App We'll be using Next.js for this guide per the [React team's recommendation](https://react.dev/learn/creating-a-react-app), but Jazz works great with vanilla React and other full-stack frameworks too. You can accept the defaults for all the questions, or customise the project as you like. ```sh npx create-next-app@latest --typescript jazzfest cd jazzfest ``` **Note: Requires Node.js 20+** ## Install Jazz The `jazz-tools` package includes everything you're going to need to build your first Jazz app. ```sh npm install jazz-tools ``` ## Get your free API key Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. **File name: .env** ```bash NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Define your schema Jazz uses Zod for more simple data types (like strings, numbers, booleans), and its own schemas to create collaborative data structures known as CoValues. CoValues are automatically persisted across your devices and the cloud and synced in real-time. Here we're defining a schema made up of both Zod types and CoValues. Adding a `root` to the user's account gives us a container that can be used to keep a track of all the data a user might need to use the app. The migration runs when the user logs in, and ensures the account is properly set up before we try to use it. **File name: app/schema.ts** ```ts import { co, z } from "jazz-tools"; export const Band = co.map({ name: z.string(), // Zod primitive type }); export const Festival = co.list(Band); export const JazzFestAccountRoot = co.map({ myFestival: Festival, }); export const JazzFestAccount = co .account({ root: JazzFestAccountRoot, profile: co.profile(), }) .withMigration((account) => { if (!account.$jazz.has("root")) { account.$jazz.set("root", { myFestival: [], }); } }); ``` ## Add the Jazz Provider \[!framework=react,svelte\] Wrap your app with a provider so components can use Jazz. **File name: app/components/JazzWrapper.tsx** ```tsx "use client"; // tells Next.js that this component can't be server-side rendered. If you're not using Next.js, you can remove it. import { JazzReactProvider } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; const apiKey = process.env.NEXT_PUBLIC_JAZZ_API_KEY; export function JazzWrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **File name: app/layout.tsx** ```tsx import { JazzWrapper } from "@/app/components/JazzWrapper"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ## Start your app Moment of truth — time to start your app and see if it works. ```bash npm run dev ``` If everything's going according to plan, you should see the default Next.js welcome page! ### Not loading? If you're not seeing the welcome page: * Check you wrapped your app with the Jazz Provider in `app/layout.tsx` * Check your schema is properly defined in `app/schema.ts` **Info: Still stuck?** Ask for help on [Discord](https://discord.gg/utDMjHYg42)! ## Create data Let's create a simple form to add a new band to the festival. We'll use the `useAccount` hook to get the current account and tell Jazz to load the `myFestival` CoValue by passing a `resolve` query. **File name: app/components/NewBand.tsx** ```tsx "use client"; import { useAccount } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; import { useState } from "react"; export function NewBand() { const me = useAccount(JazzFestAccount, { resolve: { root: { myFestival: true } }, }); const [name, setName] = useState(""); const handleSave = () => { if (!me.$isLoaded) return; me.root.myFestival.$jazz.push({ name }); setName(""); }; return (
setName(e.target.value)} />
); } ``` ## Display your data Now we've got a way to create data, so let's add a component to display it. **File name: app/components/Festival.tsx** ```tsx "use client"; import { useAccount } from "jazz-tools/react"; import { JazzFestAccount } from "@/app/schema"; export function Festival() { const me = useAccount(JazzFestAccount, { resolve: { root: { myFestival: { $each: true } } }, }); if (!me.$isLoaded) return null; return (
    {me.root.myFestival.map( (band) => band &&
  • {band.name}
  • , )}
); } ``` ## Put it all together You've built all your components, time to put them together. **File name: app/page.tsx** ```tsx import { Festival } from "@/app/components/Festival"; import { NewBand } from "@/app/components/NewBand"; export default function Home() { return (

🎪 My Festival

); } ``` You should now be able to add a band to your festival, and see it appear in the list! **Congratulations! 🎉** You've built your first Jazz app! You've begun to scratch the surface of what's possible with Jazz. Behind the scenes, your local-first JazzFest app is **already** securely syncing your data to the cloud in real-time, ready for you to build more and more powerful features. Psst! Got a few more minutes and want to add Server Side Rendering to your app? [We've got you covered!](/docs/server-side/ssr) ## Next steps * [Add authentication](/docs/key-features/authentication/quickstart) to your app so that you can log in and view your data wherever you are! * Dive deeper into the collaborative data structures we call [CoValues](/docs/core-concepts/covalues/overview) * Learn how to share and [collaborate on data](/docs/permissions-and-sharing/overview) using groups and permissions * Complete the [server-side quickstart](/docs/server-side/quickstart) to learn more about Jazz on the server ### Installation # Providers `` is the core component that connects your React application to Jazz. It handles: * **Data Synchronization**: Manages connections to peers and the Jazz cloud * **Local Storage**: Persists data locally between app sessions * **Schema Types**: Provides APIs for the [AccountSchema](/docs/core-concepts/schemas/accounts-and-migrations) * **Authentication**: Connects your authentication system to Jazz Our [Chat example app](https://jazz.tools/examples#chat) provides a complete implementation of JazzReactProvider with authentication and real-time data sync. ## Setting up the Provider The provider accepts several configuration options: ```tsx import { JazzReactProvider } from "jazz-tools/react"; import { MyAppAccount } from "./schema"; export function MyApp({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **Info: Tip** Sign up for a free API key at [dashboard.jazz.tools](https://dashboard.jazz.tools) for higher limits or production use, or use your email address as a temporary key to get started quickly. **File name: .env** ```bash NEXT_PUBLIC_JAZZ_API_KEY="you@example.com" # or your API key ``` ## Provider Options ### Sync Options The `sync` property configures how your application connects to the Jazz network: ```ts import { type SyncConfig } from "jazz-tools"; export const syncConfig: SyncConfig = { // Connection to Jazz Cloud or your own sync server peer: `wss://cloud.jazz.tools/?key=${apiKey}`, // When to sync: "always" (default), "never", or "signedUp" when: "always", }; ``` See [Authentication States](/docs/key-features/authentication/authentication-states#controlling-sync-for-different-authentication-states) for more details on how the `when` property affects synchronization based on authentication state. ### Account Schema The `AccountSchema` property defines your application's account structure: ```tsx import { JazzReactProvider } from "jazz-tools/react"; import { MyAppAccount } from "./schema"; export function MyApp({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Additional Options The provider accepts these additional options: ```tsx export function MyApp({ children }: { children: React.ReactNode }) { return ( { console.log("User logged out"); }} // Handle anonymous account data when user logs in to existing account onAnonymousAccountDiscarded={(account) => { console.log("Anonymous account discarded", account.$jazz.id); // Migrate data here return Promise.resolve(); }} > {children} ); } ``` See [Authentication States](/docs/key-features/authentication/authentication-states) for more information on authentication states, guest mode, and handling anonymous accounts. ## Authentication The Jazz Provider works with various authentication methods to enable users to access their data across multiple devices. For a complete guide to authentication, see our [Authentication Overview](/docs/key-features/authentication/overview). ## Need Help? If you have questions about configuring the Jazz Provider for your specific use case, [join our Discord community](https://discord.gg/utDMjHYg42) for help. ### API Reference # CoValues API Reference Understanding how to work with CoValues is critical to building apps with Jazz. This reference guide is intended to help you get up and running by quickly demonstrating the most common use cases. For more in depth detail, you should review the linked dedicated pages. If you have any questions, we'd be happy to chat on our [Discord server](https://discord.gg/utDMjHYg42)! | TypeScript Type | Corresponding CoValue | Usage | | -------------------------- | -------------------------- | ------------------------------------------------------- | | object | **CoMap** | Key-value stores with pre-defined keys (struct-like) | | Record | **CoRecord** | Key-value stores with arbitrary string keys (dict-like) | | T\[\] | **CoList** | Lists | | T\[\] (append-only) | **CoFeed** | Session-based append-only lists | | string | **CoPlainText/CoRichText** | Collaborative text | | Blob \| File | **FileStream** | Files | | Blob \| File (image) | **ImageDefinition** | Images | | number\[\] \| Float32Array | **CoVector** | Embeddings | | T \| U (discriminated) | **DiscriminatedUnion** | Lists of different types of items | ## Defining Schemas CoValues are defined using schemas which combine CoValue types with Zod schemas. You can find out more about schemas in the [schemas](/core-concepts/covalues/overview#start-your-app-with-a-schema) section of the overview guide. **File name: schema.ts** ```ts // 1. CoMaps: Object-like with fixed keys const ToDo = co.map({ task: z.string(), completed: z.boolean(), dueDate: z.date().optional(), }); // 2. CoRecords: Object-like with arbitrary string keys const PhoneBook = co.record(z.string(), z.string()); // co.record(keyType, valueType) // 3. CoLists: Array-like ordered list const ToDoList = co.list(ToDo); // co.list(itemType) // 4. CoFeeds: Array-like append-only list const Message = co.map({ text: z.string() }); const ChatMessages = co.feed(Message); // co.feed(itemType) // 5. CoPlainTexts/CoRichTexts: String-like const Description = co.plainText(); // or co.richText(); // 6. FileStreams: Blob-like const UploadedPDF = co.fileStream(); // 7. ImageDefinitions: Blob-like const UploadedImage = co.image(); // 8. CoVectors: Array-like list of numbers/Float32Array const Embedding = co.vector(384); // co.vector(dimensions) // 9. DiscriminatedUnions: Union of different types of items const ThisSchema = co.map({ type: z.literal("this"), thisProperty: z.string(), }); const ThatSchema = co.map({ type: z.literal("that"), thatProperty: z.string(), }); const MyThisOrThat = co.discriminatedUnion("type", [ThisSchema, ThatSchema]); // co.discriminatedUnion(discriminatorKey, arrayOfSchemas) ``` You can use the following Zod types to describe primitive data types: | Type | Usage | Comment | | ------------------ | -------- | ----------------------------------------------------------------- | | z.string() | string | For simple strings which don't need character-level collaboration | | z.number() | number | | | z.boolean() | boolean | | | z.date() | Date | | | z.literal() | literal | For enums — pass possible values as an array | | z.object() | object | An immutable, **non-collaborative** object | | z.tuple() | tuple | An immutable, **non-collaborative** array | | z.optional(schema) | optional | Pass a Zod schema for an optional property with that schema type | **Info: Tip** You can also use the `.optional()` method on both CoValue and Zod schemas to mark them as optional. There are three additional purpose-specific variants of the `CoMap` type you are likely to need while building Jazz applications. * `co.account()` — a Jazz account * `co.profile()` — a user profile * `co.group()` — a group of users ## Creating CoValues ### Explicit Creation Once you have a schema, you can create new CoValue instances using that schema using the `.create()` static method. You should pass an initial value as the first argument to this method. | CoValue Type | Example | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CoMap** | Task.create({ task: "Check out Jazz", completed: false }) | | **CoRecord** | PhoneBook.create({ "Jenny": "867-5309" }) | | **CoList** | TaskList.create(\[task1, task2\]) | | **CoPlainText/CoRichText** | Description.create("Hello World") | | **CoFeed** | ChatMessages.create(\[{ message: "Hello world!" }\]) | | **FileStream** | UploadedPDF.createFromBlob(myFile) | | **ImageDefinition** | createImage(myFile) _note that [using the helper](/docs/core-concepts/covalues/imagedef#creating-images) is the preferred way to create an ImageDefinition_ | | **CoVector** | Embedding.create(\[0.1, 0.2, ...\]) | | **DiscriminatedUnion** | MyThis.create({ type: "this", ... })_note that you can only instantiate one of the schemas in the union_ | **Info: create vs. createFromBlob** `FileStream` CoValues _can_ be created using the `.create()` method. This will create an empty `FileStream` for you to push chunks into (useful for advanced streaming cases). However, in many cases, using `.createFromBlob(blobOrFile)` to create a `FileStream` directly from a `File` or `Blob` will be more convenient. ### Inline Creation Where a schema has references, you can create nested CoValues one by one and attach them, but Jazz also allows you to create them inline by specifying their initial values. ```ts const Task = co.map({ title: z.string(), completed: z.boolean(), }); const TaskList = co.list(Task); const taskList = TaskList.create([ { title: "Task 1", completed: false }, // These will create new Task CoValues { title: "Task 2", completed: false }, // both will be inserted into the TaskList ]); ``` ### Permissions When creating any CoValue, the `.create` method accepts an optional options object as the second argument, which allows you to specify the `owner` of the CoValue. ```ts const group = co.group().create(); const task = Task.create( { title: "Buy milk", completed: false }, { owner: group }, ); ``` If you don't pass an `options` object, or if `owner` is omitted, Jazz will check if there are [permissions configured at a schema level](/docs/permissions-and-sharing/overview#defining-permissions-at-the-schema-level). If no permissions are set at a schema level when creating CoValues inline, a new group will be created extending the **containing CoValue's ownership group**. In the "Inline Creation" example above, a new group would be created for each task, each extending the ownership group of the `taskList` CoList. It is a good idea to read through the [permissions](/docs/permissions-and-sharing/overview) section to understand how to manage permissions on CoValues, as unlike in other databases, permissions are fundamental to how Jazz works at a low level, rather than a supporting feature. ## Loading and Reading CoValues In order to read data, you need to [load a CoValue instance](/docs/core-concepts/subscription-and-loading). There are several ways to do this. We recommend using Jazz with a framework, as this allows you to create reactive subscriptions to CoValues easily, but it is also possible to load a CoValue instance using the `.load()` static method on the schema. Once you have a loaded CoValue instance, you can normally read it similarly to the corresponding TypeScript type. ### CoMap (and the CoRecord sub-type) Behaves like a TypeScript object **when reading**. ```ts // CoMap: Access fixed keys console.log(user.name); // "Alice" // CoRecord: Access arbitrary keys const phone = phoneBook["Jenny"]; // Iteration works as with a TypeScript object for (const [name, number] of Object.entries(phoneBook)) { console.log(name, number); } ``` [Read more →](/docs/core-concepts/covalues/comaps) ### CoList Behaves like a TypeScript array **when reading**. ```ts const firstTask = taskList[0]; const length = taskList.length; // Iteration works as with a TypeScript array taskList.map((task) => console.log(task.title)); for (const task of taskList) { // Do something } ``` [Read more →](/docs/core-concepts/covalues/colists) ### CoPlainText/CoRichText Behaves like a TypeScript string **when reading**. ```ts // String operations const summary = description.substring(0, 100); ``` [Read more →](/docs/core-concepts/covalues/cotexts) **Note**: Although CoPlainTexts/CoRichTexts behave the same as strings in most circumstances, they are not strings. If you need an actual string type, you can use the `toString()` method. ### CoFeed CoFeeds do not correspond neatly to a TypeScript type. They are collaborative streams of entries split by session/account, and so there are various ways to access the underlying data. ```ts // Get the feed for a specific session (e.g. this browser tab) const thisSessionsFeed = chatMessages.perSession[thisSessionId]; // or .inCurrentSession as shorthand const latestMessageFromThisSession = thisSessionsFeed.value; const allMessagesFromThisSession = thisSessionsFeed.all; // Get the feed for a specific account const accountFeed = chatMessages.perAccount[accountId]; const latestMessageFromThisAccount = accountFeed.value; const allMessagesFromThisAccount = accountFeed.all; // Get the feed for my account const myFeed = chatMessages.byMe; // shorthand for chatMessages.perAccount[myAccountId] const latestMessageFromMyAccount = myFeed?.value; const allMessagesFromMyAccount = myFeed?.all; // Iterate over all entries in a CoFeed for (const userId of Object.keys(chatMessages.perAccount)) { const accountFeed = chatMessages.perAccount[userId]; for (const entry of accountFeed.all) { if (entry.value.$isLoaded) { console.log(entry.value); } } } ``` The `.all` property allows you to iterate over all entries in a per-session or per-account feed. If you need to convert a feed to an array, you can use `Array.from()` or the spread operator. [Read more →](/docs/core-concepts/covalues/cofeeds) CoFeed Structure ```text ┌────────┐ │ CoFeed └────────────────────────────────────────────────────────────┐ │ ┌────────┐ │ │ │ userA └────────────────────────────────────────────────────────┐ │ │ │ ┌───────────┐ │ │ │ │ │ Session 1 └─────────────────────────────────────────────────┐ │ │ │ │ │ ┌────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ │ │ value: someVal │ │ value: someVal2 │ │ value: someVal3 │ │ │ │ │ │ │ │ by: userA │ │ by: userA │ │ by: userA │ │ │ │ │ │ │ │ madeAt: 10:00 │ │ madeAt: 10:01 │ │ madeAt: 10:02 │ │ │ │ │ │ │ └────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────┐ │ │ │ │ │ Session 2 └─────────────────────────────────────────────────┐ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ value: someVal3 │ │ │ │ │ │ │ │ by: userA │ │ │ │ │ │ │ │ madeAt: 12:00 │ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ ┌───────┐ │ │ │ userB └─────────────────────────────────────────────────────────┐ │ │ │ ┌───────────┐ │ │ │ │ │ Session 1 └─────────────────────────────────────────────────┐ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ value: someVal4 │ │ │ │ │ │ │ │ by: userB │ │ │ │ │ │ │ │ madeAt: 10:05 │ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### FileStream FileStreams can be converted to `Blob` types or read as binary chunks. ```ts // Get raw data chunks and metadata. // Optionally pass { allowUnfinished: true } to get chunks of a FileStream which is not yet fully synced. const fileData = fileStream.getChunks({ allowUnfinished: true }); // Convert to a Blob for use in a tag or