Compare commits

..

77 Commits

Author SHA1 Message Date
Anselm
48279ed642 Clearer API 2025-06-23 10:09:27 +01:00
Anselm
88dbfefd17 Initial interface draft 2025-06-12 19:42:27 +01:00
Trisha Lim
2939790335 Merge pull request #2306 from garden-co/feat/svelte-starter
svelte starter app
2025-06-12 16:18:41 +01:00
Trisha Lim
48c29435bc replace Loaded with co.loaded 2025-06-12 15:53:54 +01:00
Trisha Lim
8668906376 refactor helper methods in starters 2025-06-12 15:53:54 +01:00
Trisha Lim
6d84e9e83f remove changelog 2025-06-12 15:53:54 +01:00
Trisha Lim
1fea0ef69c match prettier with monorepo biome as much as possible 2025-06-12 15:53:54 +01:00
Trisha Lim
e4314accb6 upgrade vite 2025-06-12 15:53:54 +01:00
Trisha Lim
ee3a4048ef missing test command 2025-06-12 15:53:54 +01:00
Trisha Lim
9ee1edef3b update lock file 2025-06-12 15:53:53 +01:00
Trisha Lim
8ab5a09a86 fix vite and tailwind config 2025-06-12 15:53:53 +01:00
Trisha Lim
2624442903 add playwright tests 2025-06-12 15:53:52 +01:00
Trisha Lim
2d199089d5 svelte starter 2025-06-12 15:53:14 +01:00
Trisha Lim
683c170b9d svelte starter 2025-06-12 15:53:14 +01:00
Benjamin S. Leveritt
518406e23d Merge pull request #2497 from garden-co/benjamin-gco-546-throw-on-typecheck-failures-with-twoslash
Throw on typecheck failures with twoslash
2025-06-12 12:39:53 +01:00
Nikos Papadopoulos
4dcbafa058 Merge pull request #2508 from garden-co/2503-create-invitation-onclick-rather-than-pre-emptively-in-org-example
organization example app: Invitation creation moved to button click event
2025-06-12 13:06:09 +02:00
Benjamin S. Leveritt
7ae9e01848 Fix. 2025-06-12 11:27:13 +01:00
Benjamin S. Leveritt
dd9ecf660d Fix? 2025-06-12 11:24:22 +01:00
Benjamin S. Leveritt
4f849050dc Fix? 2025-06-12 11:17:33 +01:00
Benjamin S. Leveritt
681600220f Excludes homepage from catalog deps 2025-06-12 10:36:26 +01:00
Benjamin S. Leveritt
384e239ad5 Bumps next to 15 in design system 2025-06-12 10:25:34 +01:00
Benjamin S. Leveritt
54e1a09a46 Bumps next to 15 2025-06-12 10:24:07 +01:00
Benjamin S. Leveritt
392a9c5aac Moves react deps to catalogs 2025-06-12 10:22:28 +01:00
Benjamin S. Leveritt
478334eabf Merge pull request #2482 from garden-co/2480-add-more-context-to-introduction-from-the-homepage
Adds context to the introduction page
2025-06-12 08:49:10 +01:00
Benjamin S. Leveritt
479f9b0113 Adds extends to turbo configs
As they're in a workspace
2025-06-12 08:33:16 +01:00
Benjamin S. Leveritt
812622b161 Adds package manager to package.json 2025-06-12 08:25:59 +01:00
Benjamin S. Leveritt
8b35fae4b6 Adds an error suppression for old code 2025-06-12 08:20:53 +01:00
Benjamin S. Leveritt
9e2ecb0378 Bumps React to 19 2025-06-12 08:11:54 +01:00
Guido D'Orsi
fe908b5300 Merge pull request #2502 from garden-co/changeset-release/main
Version Packages
2025-06-11 19:03:46 +02:00
github-actions[bot]
1f99807971 Version Packages 2025-06-11 16:52:35 +00:00
Guido D'Orsi
47f54c4d81 Merge pull request #2490 from garden-co/fix/svelte-auth-state
feat(svelte): add isAuthenticated state to AccountCoState & fix id reactivity edge cases
2025-06-11 18:44:31 +02:00
Guido D'Orsi
814f65acaf Merge pull request #2509 from garden-co/fix/rn-db-connection-reuse
fix(rn): reuse the same db connection when doing login/logout
2025-06-11 18:44:08 +02:00
Guido D'Orsi
78fd4c86a1 Merge pull request #2506 from garden-co/fix/clerk-test
fix(clerk example): missing api key in e2e test
2025-06-11 18:37:25 +02:00
Guido D'Orsi
678f326bc1 fix(rn): reuse the same db connection when doing login/logout 2025-06-11 18:25:07 +02:00
Guido D'Orsi
ea33ad4864 fix(svelte): fix some edge cases in the id reactivity on CoState 2025-06-11 18:18:28 +02:00
Guido D'Orsi
57b6d5efb4 test: skip failing tests on PasskeyAuthBasicUI/svelte 2025-06-11 18:18:09 +02:00
Guido D'Orsi
15929b121d feat(svelte): add isAuthenticated state to AccountCoState 2025-06-11 18:18:09 +02:00
Nikos Papadopoulos
6edd061202 removes invite link state 2025-06-11 17:07:07 +01:00
Trisha Lim
eb4cef196c inspector: add tests with playwright (#2500)
* test account dropdown

* remove unused packages

* test account

* attempt test covalues

* test: add createAccount utility

* move things around

* improve locator text

* add inspector to playwright.yml

---------

Co-authored-by: Guido D'Orsi <gu.dorsi@gmail.com>
2025-06-11 17:05:14 +01:00
Emil Sayahi
85703f9241 fix: remove .env from betterauth example (#2507)
* fix: remove `.env` from `betterauth` example

* fix: match convention
2025-06-11 08:59:39 -07:00
Nikos Papadopoulos
865d5385e9 moves invitation creation to button click event in organization example app 2025-06-11 16:55:18 +01:00
Trisha Lim
5bb12523ee create-jazz-app: delete .env.test file 2025-06-11 16:37:26 +01:00
Trisha Lim
190cb1efb2 copy .env.test to .env in test 2025-06-11 16:15:52 +01:00
Trisha Lim
46ab1f6db2 try .env.test 2025-06-11 15:55:29 +01:00
Trisha Lim
a44fc6fc6d move env to build step 2025-06-11 15:36:29 +01:00
Trisha Lim
2376a8d3b2 add .env.example to clerk examples 2025-06-11 15:34:05 +01:00
Trisha Lim
2c3ec2fea6 fix(clerk example): missing api key in e2e test 2025-06-11 15:26:35 +01:00
Nikos Papadopoulos
da6f6ec4d5 Merge pull request #2496 from garden-co/2462-add-jazz-inspector-to-all-example-apps
integrates inspector to all example apps
2025-06-11 16:21:38 +02:00
Guido D'Orsi
8c78b37bfb Merge pull request #2492 from garden-co/feat/add-error-boundary-for-inspector
Add error boundary for inspector
2025-06-11 16:18:03 +02:00
Nikos Papadopoulos
5f382309de updates pnpm-lock.yaml 2025-06-11 15:04:31 +01:00
Divya
aa7eb3cf2c fix: ts error with error type 2025-06-11 09:36:23 -04:00
Divya
9b41762e96 fix: styling 2025-06-11 09:23:07 -04:00
Nikos Papadopoulos
28be460286 Merge pull request #2504 from garden-co/nikos/clerk_rn_env_ignore
fix for clerk rn example app .env
2025-06-11 15:22:27 +02:00
Divya
df8af06814 fix: pr feedback 2025-06-11 09:14:04 -04:00
Divya
2ef460fccf fix: build errors and style tweaks 2025-06-11 09:14:04 -04:00
Divya
9660e2c03c feat: Add error boundary to inspector so we can tell if a coId is invalid 2025-06-11 09:14:04 -04:00
Nikos Papadopoulos
908645e4b7 removes clerk rn example app .env and updates .gitignore rules 2025-06-11 14:11:29 +01:00
Nikos Papadopoulos
f3ca37ed5e Merge pull request #2501 from garden-co/nikos/clerk_env_ignore
fix for clerk example app .env
2025-06-11 14:57:03 +02:00
Nikos Papadopoulos
a9d0fd14c4 removes unused inspector dependency from passkey-svelte package.json 2025-06-11 13:51:14 +01:00
Guido D'Orsi
c496f49bb0 Merge pull request #2499 from garden-co/2498-fix-progressiveimg-cannot-be-used-as-a-jsx-component
Fix `ProgressiveImg` cannot be used as a jsx component
2025-06-11 14:42:09 +02:00
Nikos Papadopoulos
b26666ab4c removes clerk example app .env and updates .gitignore rules 2025-06-11 13:26:20 +01:00
Benjamin S. Leveritt
9088a349a0 Adds changeset 2025-06-11 12:49:35 +01:00
Benjamin S. Leveritt
54b12dcb7a Returns explicit type for ProgressiveImg 2025-06-11 12:47:34 +01:00
Benjamin S. Leveritt
a998f94789 Includes the recommendation in the error 2025-06-11 12:46:53 +01:00
Benjamin S. Leveritt
d17eecfe16 Removes z.null from docs 2025-06-11 12:19:29 +01:00
Nikos Papadopoulos
71b9a5ce25 updates clerk example .gitignore to add .env exclusion 2025-06-11 11:55:17 +01:00
Benjamin S. Leveritt
8ebfbc86db Rethrows in production 2025-06-11 11:36:56 +01:00
Benjamin S. Leveritt
abad8e762f Changes config to throw on typecheck error 2025-06-11 11:21:49 +01:00
Benjamin S. Leveritt
037e16392e Removes list of frameworks 2025-06-11 10:55:19 +01:00
Benjamin S. Leveritt
49ac65c123 Reorders list 2025-06-11 10:55:19 +01:00
Benjamin S. Leveritt
3510fb1273 Update homepage/homepage/content/docs/index.mdx
Co-authored-by: Anselm Eickhoff <anselm.eickhoff@gmail.com>
2025-06-11 10:55:19 +01:00
Benjamin S. Leveritt
bc3efe7ca0 Tweaks 2025-06-11 10:55:19 +01:00
Benjamin S. Leveritt
3b06a7809e Adds why and how sections 2025-06-11 10:55:18 +01:00
Benjamin S. Leveritt
58aa04bb10 Adds introduction paragraph 2025-06-11 10:55:18 +01:00
Trisha Lim
afb94ef043 Merge pull request #2487 from garden-co/feat/link-to-frameworks
link to framework docs from homepage
2025-06-11 10:34:29 +01:00
Nikos Papadopoulos
7554dd9f88 integrates inspector to all example apps 2025-06-11 10:13:21 +01:00
Trisha Lim
4f0fb6c27f link to framework docs from homepage 2025-06-10 22:59:51 +01:00
190 changed files with 5268 additions and 2008 deletions

View File

@@ -0,0 +1,5 @@
---
"create-jazz-app": patch
---
use new svelte starter

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
starter: ["react-passkey-auth"]
starter: ["react-passkey-auth", "svelte-passkey-auth"]
steps:
- uses: actions/checkout@v4

View File

@@ -22,6 +22,9 @@ jobs:
- name: Setup Source Code
uses: ./.github/actions/source-code/
- name: Install root dependencies
run: pnpm install && pnpm turbo build
- name: Install project dependencies
run: pnpm install
working-directory: ./${{ matrix.project }}

View File

@@ -13,7 +13,21 @@ jobs:
continue-on-error: true
strategy:
matrix:
project: ["tests/e2e", "examples/chat", "examples/clerk", "examples/betterauth", "examples/file-share-svelte", "examples/form", "examples/music-player", "examples/organization", "examples/pets", "starters/react-passkey-auth"]
project: [
"tests/e2e",
"examples/chat",
"examples/clerk",
"examples/betterauth",
"examples/file-share-svelte",
"examples/form",
"examples/inspector",
"examples/music-player",
"examples/organization",
"examples/pets",
"starters/react-passkey-auth",
"starters/svelte-passkey-auth",
"packages/jazz-svelte"
]
steps:
- uses: actions/checkout@v4
@@ -24,7 +38,11 @@ jobs:
uses: ./.github/actions/source-code/
- name: Pnpm Build
run: pnpm turbo build
run: |
if [ -f .env.test ]; then
cp .env.test .env
fi
pnpm turbo build
working-directory: ./${{ matrix.project }}
- name: Install Playwright Browsers

View File

@@ -13,6 +13,7 @@
"**/android/**",
"packages/jazz-svelte/**",
"examples/*svelte*/**",
"starters/*svelte*/**",
"examples/jazz-paper-scissors/src/routeTree.gen.ts",
"homepage/homepage/**",
"**/package.json"

View File

@@ -36,8 +36,10 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env
.env
.env.*
!.env.example
!.env.test
# vercel
.vercel

View File

@@ -1 +0,0 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

View File

@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=

View File

@@ -15,3 +15,9 @@ web-build/
ios
android
# Env
.env
.env.*
!.env.example
!.env.test

View File

@@ -0,0 +1 @@
VITE_CLERK_PUBLISHABLE_KEY=

View File

@@ -1 +1 @@
VITE_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ
VITE_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

View File

@@ -23,4 +23,10 @@ dist-ssr
*.sln
*.sw?
playwright-report
playwright-report
# Env
.env
.env.*
!.env.example
!.env.test

View File

@@ -13,6 +13,7 @@
},
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-react-auth-clerk": "workspace:*",
"jazz-tools": "workspace:*",

View File

@@ -3,6 +3,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { JazzInspector } from "jazz-inspector";
import { JazzProviderWithClerk } from "jazz-react-auth-clerk";
import { ReactNode } from "react";
import { apiKey } from "./apiKey";
@@ -44,6 +45,7 @@ if (location.search.includes("expirationTest")) {
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
<JazzProvider>
<App />
<JazzInspector />
</JazzProvider>
</ClerkProvider>
</StrictMode>,

View File

@@ -1,11 +1,3 @@
<script lang="ts" module>
declare module 'jazz-svelte' {
interface Register {
Account: typeof FileShareAccount;
}
}
</script>
<script lang="ts">
import { JazzProvider } from 'jazz-svelte';
import "jazz-inspector-element"

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"hash-slash": "workspace:*",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -15,6 +16,7 @@ createRoot(document.getElementById("root")!).render(
AccountSchema={JazzAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -10,6 +10,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -15,6 +16,7 @@ createRoot(document.getElementById("root")!).render(
AccountSchema={JazzAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -5,24 +5,25 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "playwright test",
"test:headed": "playwright test --headed",
"format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write",
"preview": "vite preview"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-tools": "workspace:*",
"clsx": "^2.0.0",
"cojson": "workspace:*",
"cojson-transport-ws": "workspace:*",
"hash-slash": "workspace:*",
"lucide-react": "^0.274.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-router": "^6.16.0",
"react-router-dom": "^6.16.0",
"react-use": "^17.4.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
"@vitejs/plugin-react-swc": "^3.10.1",

View File

@@ -0,0 +1,46 @@
import { defineConfig, devices } from "@playwright/test";
import isCI from "is-ci";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: isCI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: isCI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:5173/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
permissions: ["clipboard-read", "clipboard-write"],
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
/* Run your local dev server before starting the tests */
webServer: [
{
command: "pnpm preview --port 5173",
url: "http://localhost:5173/",
reuseExistingServer: !isCI,
},
],
});

View File

@@ -35,6 +35,7 @@ interface JazzLoggedInSecret {
}
export default function CoJsonViewerApp() {
const [errors, setErrors] = useState<string | null>(null);
const [accounts, setAccounts] = useState<Account[]>(() => {
const storedAccounts = localStorage.getItem("inspectorAccounts");
return storedAccounts ? JSON.parse(storedAccounts) : [];
@@ -80,23 +81,46 @@ export default function CoJsonViewerApp() {
websocket: new WebSocket("wss://cloud.jazz.tools"),
role: "server",
});
const node = await LocalNode.withLoadedAccount({
accountID: currentAccount.id,
accountSecret: currentAccount.secret,
sessionID: crypto.newRandomSessionID(currentAccount.id),
peersToLoadFrom: [wsPeer],
crypto,
migration: async () => {
console.log("Not running any migration in inspector");
},
});
let node;
try {
node = await LocalNode.withLoadedAccount({
accountID: currentAccount.id,
accountSecret: currentAccount.secret,
sessionID: crypto.newRandomSessionID(currentAccount.id),
peersToLoadFrom: [wsPeer],
crypto,
migration: async () => {
console.log("Not running any migration in inspector");
},
});
} catch (err: any) {
if (err.toString().includes("invalid id")) {
setAccounts(accounts.filter((acc) => acc.id !== currentAccount.id));
//remove from localStorage
localStorage.removeItem("lastSelectedAccountId");
localStorage.setItem(
"inspectorAccounts",
JSON.parse(localStorage.inspectorAccounts).filter(
(acc: Account) => acc.id != currentAccount.id,
),
);
setCurrentAccount(null);
setErrors("Trying to load covalue with invalid id");
} else {
setErrors("The account could not be loaded");
}
setLocalNode(null);
goToIndex(-1);
return;
}
setLocalNode(node);
});
}, [currentAccount, goToIndex, path]);
}, [currentAccount, accounts, goToIndex, path]);
const addAccount = (id: RawAccountID, secret: AgentSecret) => {
const newAccount = { id, secret };
const accountExists = accounts.some((account) => account.id === id);
//todo: ideally there would be some validation here so we don't have to manually remove a non existent account from localStorage
if (!accountExists) {
setAccounts([...accounts, newAccount]);
}
@@ -174,7 +198,9 @@ export default function CoJsonViewerApp() {
goBack={goBack}
addPages={addPages}
>
{!currentAccount && <AddAccountForm addAccount={addAccount} />}
{!currentAccount && (
<AddAccountForm addAccount={addAccount} errors={errors} />
)}
{currentAccount && path.length <= 0 && (
<form
@@ -270,8 +296,10 @@ function AccountSwitcher({
function AddAccountForm({
addAccount,
errors,
}: {
addAccount: (id: RawAccountID, secret: AgentSecret) => void;
errors: string | null;
}) {
const [id, setId] = useState("");
const [secret, setSecret] = useState("");
@@ -305,8 +333,17 @@ function AddAccountForm({
return (
<form
onSubmit={handleSubmit}
className="flex flex-col gap-3 max-w-md mx-auto h-full justify-center"
className={`flex flex-col max-w-[30rem] mx-auto justify-center ${errors == null ? "h-full" : ""}`}
>
{errors != null && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mt-4 font-mono whitespace-pre-wrap break-words mb-8">
<h3>Error</h3>
<pre className="whitespace-pre-wrap break-words overflow-hidden">
{errors}
</pre>
</div>
)}
<h2 className="text-2xl font-medium text-gray-900 dark:text-white">
Add an account to inspect
</h2>

View File

@@ -0,0 +1,91 @@
import { co, z } from "jazz-tools";
const projectsData: {
name: string;
description: string;
issues: {
title: string;
status: "open" | "closed";
labels: string[];
}[];
}[] = [
{
name: "Jazz",
description: "Jazz is a framework for building collaborative apps.",
issues: [
{
title: "Issue 1",
status: "open",
labels: [
"bug",
"feature",
"enhancement",
"documentation",
"homepage",
"help needed",
"requested",
"blocked",
"high priority",
"urgent",
],
},
{ title: "Issue 2", status: "closed", labels: ["bug"] },
{ title: "Issue 3", status: "open", labels: ["feature", "enhancement"] },
],
},
{
name: "Waffle",
description: "Start waffling",
issues: [],
},
{
name: "Garden",
description: "Grow your garden",
issues: [],
},
{
name: "Tilescape",
description: "",
issues: [],
},
];
const Issue = co.map({
title: z.string(),
status: z.enum(["open", "closed"]),
labels: co.list(z.string()),
});
const Project = co.map({
name: z.string(),
description: z.string(),
issues: co.list(Issue),
});
const Organization = co.map({
name: z.string(),
projects: co.list(Project),
});
export const createOrganization = () => {
return Organization.create({
name: "Garden Computing",
projects: co.list(Project).create(
projectsData.map((project) =>
Project.create({
name: project.name,
description: project.description,
issues: co.list(Issue).create(
project.issues.map((issue) =>
Issue.create({
title: issue.title,
status: issue.status,
labels: co.list(z.string()).create(issue.labels),
}),
),
),
}),
),
),
});
};

View File

@@ -0,0 +1,82 @@
import { expect, test } from "@playwright/test";
import { createOrganization } from "./data";
import { createAccount, initializeKvStore } from "./lib";
initializeKvStore();
const { account, accountID, accountSecret } = await createAccount();
test("should add and delete account in dropdown", async ({ page }) => {
await page.goto("/");
await page.getByLabel("Account ID").fill(accountID);
await page.getByLabel("Account secret").fill(accountSecret);
await page.getByRole("button", { name: "Add account" }).click();
await expect(page.getByText("Jazz CoValue Inspector")).toBeVisible();
await page
.getByLabel("Account to inspect")
.selectOption(`Inspector test account <${accountID}>`);
await page.getByRole("button", { name: "Remove account" }).click();
await expect(page.getByText("Jazz CoValue Inspector")).not.toBeVisible();
await expect(page.getByText("Add an account to inspect")).toBeVisible();
await expect(
page.getByText(`Inspector test account <${accountID}>`),
).not.toBeVisible();
});
test("should inspect account", async ({ page }) => {
await page.goto("/");
await page.getByLabel("Account ID").fill(accountID);
await page.getByLabel("Account secret").fill(accountSecret);
await page.getByRole("button", { name: "Add account" }).click();
await page.getByRole("button", { name: "Inspect my account" }).click();
await expect(page.getByRole("heading", { name: accountID })).toBeVisible();
await expect(page.getByText("👤 Account")).toBeVisible();
await page.getByRole("button", { name: "profile {} CoMap name:" }).click();
await expect(page.getByText("Role: admin")).toBeVisible();
});
test("should inspect CoValue", async ({ page }) => {
await page.goto("/");
await page.getByLabel("Account ID").fill(accountID);
await page.getByLabel("Account secret").fill(accountSecret);
await page.getByRole("button", { name: "Add account" }).click();
const organization = createOrganization();
await account.waitForAllCoValuesSync(); // Ensures that the organization is uploaded
await page.getByLabel("CoValue ID").fill(organization.id);
await page.getByRole("button", { name: "Inspect CoValue" }).click();
await expect(page.getByText(/Garden Computing/)).toHaveCount(2);
await expect(
page.getByRole("heading", { name: organization.id }),
).toBeVisible();
await expect(page.getByText("Role: admin")).toBeVisible();
await page.getByRole("button", { name: /projects/ }).click();
await expect(page.getByText("Showing 4 of 4")).toBeVisible();
await page.getByRole("button", { name: "View" }).first().click();
await expect(
page.getByText("Jazz is a framework for building collaborative apps."),
).toBeVisible();
await page.getByRole("button", { name: /issues/ }).click();
await expect(page.getByText("Showing 3 of 3")).toBeVisible();
await page.getByRole("button", { name: "View" }).first().click();
await page.getByRole("button", { name: /labels/ }).click();
// currently broken:
// await expect(page.getByText("Showing 10 of 10")).toBeVisible();
await expect(page.getByRole("table").getByRole("row")).toHaveCount(11);
await page.getByRole("button", { name: "issues" }).click();
await expect(page.getByRole("table").getByRole("row")).toHaveCount(4);
await page.getByRole("button", { name: "projects" }).click();
await expect(page.getByRole("table").getByRole("row")).toHaveCount(5);
});

View File

@@ -0,0 +1,43 @@
import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import {
AuthSecretStorage,
InMemoryKVStore,
KvStoreContext,
co,
createJazzContext,
randomSessionProvider,
z,
} from "jazz-tools";
export const initializeKvStore = () => {
const kvStore = new InMemoryKVStore();
KvStoreContext.getInstance().initialize(kvStore);
};
export async function createAccount() {
const { account, authSecretStorage } = await createJazzContext({
defaultProfileName: "Inspector test account",
crypto: await WasmCrypto.create(),
sessionProvider: randomSessionProvider,
authSecretStorage: new AuthSecretStorage(),
peersToLoadFrom: [
createWebSocketPeer({
id: "upstream",
role: "server",
websocket: new WebSocket(
"wss://cloud.jazz.tools/?key=inspector-test@jazz.tools",
),
}),
],
});
await account.waitForAllCoValuesSync();
const credentials = await authSecretStorage.get();
if (!credentials) {
throw new Error("No credentials found");
}
return { account, ...credentials };
}

View File

@@ -11,6 +11,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.2",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*"
},

View File

@@ -1,5 +1,6 @@
"use client";
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
export function Jazz({ children }: { children: React.ReactNode }) {
@@ -11,6 +12,7 @@ export function Jazz({ children }: { children: React.ReactNode }) {
}}
>
{children}
<JazzInspector />
</JazzProvider>
);
}

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@react-spring/web": "^9.7.5",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -16,6 +17,7 @@ createRoot(document.getElementById("root")!).render(
AccountSchema={CursorAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -1 +0,0 @@
VITE_CLERK_PUBLISHABLE_KEY=pk_test_ZXZpZGVudC1kYW5lLTg5LmNsZXJrLmFjY291bnRzLmRldiQ

View File

@@ -0,0 +1 @@
VITE_CLERK_PUBLISHABLE_KEY=

View File

@@ -22,3 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Env
.env
.env.*
!.env.example
!.env.test

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@clerk/clerk-react": "^5.4.1",
"jazz-react": "workspace:*",
"jazz-inspector": "workspace:*",
"jazz-react-auth-clerk": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,4 +1,5 @@
import { ClerkProvider } from "@clerk/clerk-react";
import { JazzInspector } from "jazz-inspector";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
@@ -22,6 +23,7 @@ createRoot(document.getElementById("root")!).render(
}}
>
<App />
<JazzInspector />
</OmniAuth>
</ClerkProvider>
</StrictMode>,

View File

@@ -12,6 +12,7 @@
"test:e2e:ui": "playwright test --ui"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",
@@ -21,8 +22,8 @@
"react-router-dom": "^6.16.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@biomejs/biome": "1.9.4",
"@playwright/test": "^1.50.1",
"@tailwindcss/forms": "^0.5.9",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",

View File

@@ -7,16 +7,9 @@ import { Organization } from "../schema.ts";
export function InviteLink({
organization,
}: { organization: Loaded<typeof Organization> }) {
const [inviteLink, setInviteLink] = useState<string>();
let [copyCount, setCopyCount] = useState(0);
let copied = copyCount > 0;
useEffect(() => {
if (organization) {
setInviteLink(createInviteLink(organization, "writer"));
}
}, [organization.id]);
useEffect(() => {
if (copyCount > 0) {
let timeout = setTimeout(() => setCopyCount(0), 1000);
@@ -27,11 +20,10 @@ export function InviteLink({
}, [copyCount]);
const copyUrl = () => {
if (inviteLink) {
navigator.clipboard.writeText(inviteLink).then(() => {
setCopyCount((count) => count + 1);
});
}
const inviteLink = createInviteLink(organization, "writer");
navigator.clipboard.writeText(inviteLink).then(() => {
setCopyCount((count) => count + 1);
});
};
return (

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -37,6 +38,7 @@ createRoot(document.getElementById("root")!).render(
}}
>
<Router />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -10,6 +10,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -15,6 +16,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
<PasskeyAuthBasicUI appName="Jazz Minimal Auth Passkey Example">
{children}
</PasskeyAuthBasicUI>
<JazzInspector />
</JazzProvider>
);
}

View File

@@ -10,6 +10,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, usePassphraseAuth } from "jazz-react";
import { StrictMode, useState } from "react";
import { createRoot } from "react-dom/client";
@@ -150,6 +151,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
>
{children}
</PassphraseAuthBasicUI>
<JazzInspector />
</JazzProvider>
);
}

View File

@@ -11,6 +11,7 @@
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react";
import React from "react";
import ReactDOM from "react-dom/client";
@@ -25,6 +26,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<JazzAndAuth>
<App />
<JazzInspector />
</JazzAndAuth>
</React.StrictMode>,
);

View File

@@ -18,6 +18,7 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import React from "react";
import ReactDOM from "react-dom/client";
import {
@@ -47,6 +48,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<PasskeyAuthBasicUI appName={appName}>
<App />
</PasskeyAuthBasicUI>
<JazzInspector />
</JazzProvider>
</div>
</ThemeProvider>

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"hash-slash": "workspace:*",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider, PasskeyAuthBasicUI } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -15,6 +16,7 @@ createRoot(document.getElementById("root")!).render(
<PasskeyAuthBasicUI appName="Jazz Reactions Example">
<App />
</PasskeyAuthBasicUI>
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-richtext-prosemirror": "workspace:*",
"jazz-tools": "workspace:*",

View File

@@ -1,4 +1,5 @@
import "./app.css";
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import React from "react";
import ReactDOM from "react-dom/client";
@@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
AccountSchema={JazzAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</React.StrictMode>,
);

View File

@@ -12,6 +12,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-richtext-prosemirror": "workspace:*",
"jazz-tools": "workspace:*",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -21,6 +22,7 @@ createRoot(document.getElementById("root")!).render(
AccountSchema={JazzAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -20,6 +20,7 @@
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"clsx": "^2.1.1",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-richtext-tiptap": "workspace:*",
"jazz-tools": "workspace:*",

View File

@@ -1,3 +1,4 @@
import { JazzInspector } from "jazz-inspector";
import { JazzProvider } from "jazz-react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
@@ -21,6 +22,7 @@ createRoot(document.getElementById("root")!).render(
AccountSchema={JazzAccount}
>
<App />
<JazzInspector />
</JazzProvider>
</StrictMode>,
);

View File

@@ -12,6 +12,7 @@
"format-and-lint:fix": "biome check . --write"
},
"dependencies": {
"jazz-inspector-element": "workspace:*",
"jazz-browser": "workspace:*",
"jazz-tools": "workspace:*",
"jazz-vue": "workspace:*",

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-toast": "^1.1.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"jazz-inspector": "workspace:*",
"jazz-react": "workspace:*",
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0",

View File

@@ -6,13 +6,13 @@ import {
} from "react-router-dom";
import "./index.css";
import { JazzInspector } from "jazz-inspector";
import {
JazzProvider,
PassphraseAuthBasicUI,
useAcceptInvite,
useAccount,
} from "jazz-react";
import React from "react";
import { TodoAccount, TodoProject } from "./1_schema.ts";
import { NewProjectForm } from "./3_NewProjectForm.tsx";
@@ -62,6 +62,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<App />
</div>
</ThemeProvider>
<JazzInspector />
</JazzAndAuth>
</React.StrictMode>,
);

View File

@@ -19,12 +19,12 @@
"jazz-react": "link:../../packages/jazz-react",
"jazz-tools": "link:../../packages/jazz-tools",
"lucide-react": "^0.436.0",
"next": "14.2.7",
"next": "15.2.1",
"next-themes": "^0.2.1",
"postcss": "^8",
"radix-ui": "^1.4.2",
"react": "^18",
"react-dom": "^18",
"react": "catalog:",
"react-dom": "catalog:",
"resend": "^4.0.0",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.4.17",
@@ -34,8 +34,8 @@
"@biomejs/biome": "1.9.4",
"@csstools/postcss-oklab-function": "^3.0.6",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"typescript": "^5.3.3"
}
}

View File

@@ -20,7 +20,8 @@
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"]
}
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]

View File

@@ -12,5 +12,6 @@
"persistent": true,
"dependsOn": ["^build"]
}
}
},
"extends": ["//"]
}

View File

@@ -10,8 +10,15 @@ import { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
type Params = {
params: Promise<{
slug: string;
}>;
};
export default async function Post({ params }: Params) {
const post = getPostBySlug(params.slug);
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return notFound();
@@ -65,14 +72,9 @@ export default async function Post({ params }: Params) {
);
}
type Params = {
params: {
slug: string;
};
};
export function generateMetadata({ params }: Params): Metadata {
const post = getPostBySlug(params.slug);
export async function generateMetadata({ params }: Params): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) {
return notFound();

View File

@@ -18,15 +18,17 @@ const membersNameToInfoMap = {
brad: "Bradley Kowalski",
};
export default function TeamMemberPage({
export default async function TeamMemberPage({
params,
}: { params: { member: string } }) {
if (!(params.member in membersNameToInfoMap)) {
}: { params: Promise<{ member: string }> }) {
const { member } = await params;
if (!(member in membersNameToInfoMap)) {
Router.push("/team");
}
const memberName =
membersNameToInfoMap[params.member as keyof typeof membersNameToInfoMap];
membersNameToInfoMap[member as keyof typeof membersNameToInfoMap];
const memberInfo = team.find(
(m: { name: string }) => m.name.toLowerCase() === memberName.toLowerCase(),
);

View File

@@ -26,10 +26,10 @@
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-mdx": "^3.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"next": "14.2.15",
"next": "15.2.1",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"react": "catalog:",
"react-dom": "catalog:",
"shiki": "^0.14.6",
"shiki-twoslash": "^3.1.2",
"tailwind-merge": "^1.14.0",
@@ -39,8 +39,8 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"autoprefixer": "^10",
"postcss": "^8",
"tailwindcss": "^3",

View File

@@ -7,85 +7,54 @@ import { ReactLogo } from "@/components/icons/ReactLogo";
import { ReactNativeLogo } from "@/components/icons/ReactNativeLogo";
import { SvelteLogo } from "@/components/icons/SvelteLogo";
import { GappedGrid } from "@garden-co/design-system/src/components/molecules/GappedGrid";
import Link from "next/link";
import React from "react";
const frameworks = [
{
name: "JavaScript",
icon: JavascriptLogo,
href: "/docs/vanilla",
},
{
name: "React",
icon: ReactLogo,
href: "/docs/react",
},
{
name: "React Native",
icon: ReactNativeLogo,
href: "/docs/react-native",
},
{
name: "Expo",
icon: ExpoLogo,
href: "/docs/react-native-expo",
},
{
name: "Svelte",
icon: SvelteLogo,
href: "/docs/svelte",
},
];
const serverWorkers = [
{
name: "Node.js",
icon: NodejsLogo,
href: "/docs/react/server-workers",
},
{
name: "Cloudflare Workers",
icon: CloudflareWorkerLogo,
},
{
name: "Bun",
icon: BunLogo,
},
];
export function SupportedEnvironmentsSection() {
const frameworks = [
{
name: "JavaScript",
icon: JavascriptLogo,
},
{
name: "React",
icon: ReactLogo,
},
{
name: "React Native",
icon: ReactNativeLogo,
},
{
name: "Expo",
icon: ExpoLogo,
},
{
name: "Svelte",
icon: SvelteLogo,
},
];
const serverWorkers = [
{
name: "Node.js",
icon: NodejsLogo,
},
{
name: "Cloudflare Workers",
icon: CloudflareWorkerLogo,
},
{
name: "Bun",
icon: BunLogo,
},
];
const first = [
{
name: "JavaScript",
icon: JavascriptLogo,
},
{
name: "React",
icon: ReactLogo,
},
{
name: "Svelte",
icon: SvelteLogo,
},
{
name: "Expo",
icon: ExpoLogo,
},
{
name: "React Native",
icon: ReactNativeLogo,
},
];
const second = [
{
name: "Node.js",
icon: NodejsLogo,
},
{
name: "Cloudflare Workers",
icon: CloudflareWorkerLogo,
},
{
name: "Bun",
icon: BunLogo,
},
];
return (
<>
<h2 className="sr-only">Supported environments</h2>
@@ -102,13 +71,27 @@ export function SupportedEnvironmentsSection() {
].map(({ label, items }) => (
<div className="col-span-2 lg:col-span-3" key={label}>
<h3 className="mb-4 text-highlight font-medium">{label}</h3>
<div className="flex gap-x-6 gap-y-3 grayscale flex-col lg:flex-row">
{items.map(({ name, icon: Icon }) => (
<div key={name} className="flex items-center gap-2">
<Icon className="size-6" />
{name}
</div>
))}
<div className="flex gap-x-6 gap-y-3 flex-col lg:flex-row">
{items.map(({ name, icon: Icon, href }) => {
if (href) {
return (
<Link
href={href}
key={name}
className="flex items-center gap-2 grayscale hover:grayscale-0"
>
<Icon className="size-6" />
{name}
</Link>
);
}
return (
<div key={name} className="flex items-center gap-2 grayscale">
<Icon className="size-6" />
{name}
</div>
);
})}
</div>
</div>
))}

View File

@@ -6,7 +6,11 @@ export const metadata = {
# Learn some <span className="sr-only">Jazz</span> <JazzLogo className="h-[41px] -ml-0.5 -mt-[3px] inline" />
Welcome to the Jazz documentation!
**Jazz is a toolkit for building backendless apps**. You get data without needing a database — plus auth, permissions, files and multiplayer without needing a backend. Jazz lets you do everything right from the frontend and you'll ship better apps, faster.
Instead of wrestling with databases, APIs, and server infrastructure, you work with **CoValues** ("collaborative values") — your new cloud-synced building blocks that feel like local state but automatically sync across all devices and users in real-time.
---
**Note:** We just released [Jazz 0.14.0](/docs/upgrade/0-14-0) with a bunch of breaking changes and are still cleaning the docs up - see the [upgrade guide](/docs/upgrade/0-14-0) for details.
@@ -20,37 +24,44 @@ npx create-jazz-app@latest --api-key you@example.com
```
</CodeGroup>
Or set up Jazz yourself, using the following instructions for your framework of choice:
- [React](/docs/react/project-setup)
- [Next.js](/docs/react/project-setup#nextjs)
- [React Native](/docs/react-native/project-setup)
- [React Native Expo](/docs/react-native-expo/project-setup)
- [Vue](/docs/vue/project-setup)
- [Svelte](/docs/svelte/project-setup)
{/* <ContentByFramework framework="react">
Or you can follow this [React step-by-step guide](/docs/react/guide) where we walk you through building an issue tracker app.
</ContentByFramework> */}
## Example apps
## Why Jazz is different
You can also find [example apps](/examples) with code most similar to what you want to build. These apps
make use of different features such as auth, file upload, and more.
Most apps rebuild the same thing: shared state that syncs between users and devices. Jazz starts from that shared state, giving you:
- **No backend required** — Focus on building features, not infrastructure
- **Real-time sync** — Changes appear everywhere immediately
- **Multiplayer by default** — Collaboration just works
- **Local-first** — Your app works offline and feels instant
Think Figma, Notion, or Linear — but you don't need years to build a custom stack.
## How it works
1. **Define your data** with CoValues schemas
2. **Connect to sync infrastructure** (Jazz Cloud or self-hosted)
3. **Create and edit CoValues** like normal objects
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.
## Ready to see Jazz in action?
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.
## Core concepts
Learn how to structure your data using [collaborative values](/docs/schemas/covalues) — the building blocks that make Jazz apps work.
## Sync and storage
Sync and persist your data by setting up a [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or do it yourself.
Sync and persist your data by setting up [sync and storage infrastructure](/docs/sync-and-storage) using Jazz Cloud, or host it yourself.
## Collaborative values
Learn how to structure your data using [collaborative values](/docs/schemas/covalues).
## LLM Docs
## Going deeper
Get better results with AI by [importing the Jazz docs](/docs/ai-tools) into your context window.
## Get support
If you have any questions or need assistance, please don't hesitate to reach out to us on [Discord](https://discord.gg/utDMjHYg42).
We would love to help you get started.
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.

View File

@@ -253,14 +253,13 @@ Here's a quick overview of the primitive types you can use:
<CodeGroup>
```ts twoslash
import {z} from "jazz-tools";
// ---cut---
z.string(); // For simple strings
z.number(); // For numbers
import { z } from "jazz-tools";
// ---cut---
z.string(); // For simple strings
z.number(); // For numbers
z.boolean(); // For booleans
z.null(); // For null
z.date(); // For dates
z.literal(["waiting", "ready"]); // For enums
z.date(); // For dates
z.literal(["waiting", "ready"]); // For enums
```
</CodeGroup>

View File

@@ -24,7 +24,7 @@ We're introducing a new resolve API for deep loading, more friendly to TypeScrip
<CodeGroup>
```tsx twoslash
// @noErrors: 2451
// @noErrors: 2451 2769
import { CoMap, CoList, coField, Account } from "jazz-tools";
import { useAccount } from "jazz-react";
class AccountRoot extends CoMap { friends = coField.ref(ListOfAccounts); }

View File

@@ -84,18 +84,22 @@ function highlightPlugin() {
transformers: [
transformerTwoslash({
explicitTrigger: true,
throws: false, //process.env.NODE_ENV === "production",
onTwoslashError:
process.env.NODE_ENV !== "production"
? (e, code) => {
const { description, recommendation } = e;
console.error("\nTwoslash error: ");
console.log(description);
console.log(recommendation);
console.log("\nCode: \n```\n" + code + "\n```");
error = e;
}
: undefined,
throws: process.env.NODE_ENV === "production",
onTwoslashError: (e, code) => {
if (process.env.NODE_ENV === "production") {
// Re-throw to actually fail the build in production
throw e;
}
const { description, recommendation } = e;
console.error("\nTwoslash error: ");
console.log(description);
console.log(recommendation);
console.log("\nCode: \n```\n" + code + "\n```");
// In development, store the error to show inline
error = e;
},
}),
transformerNotationDiff(),
],
@@ -103,7 +107,7 @@ function highlightPlugin() {
node.type = "html";
node.value = error
? `<div style="color: red;">${error}</div>` + html
? `<div style="color: red; background: #fee; padding: 8px; border: 1px solid #fcc; margin: 8px 0;"><strong>Twoslash Error:</strong> ${error.description || error.message} ${error.recommendation}</div>` + html
: html;
node.children = [];
return SKIP;

View File

@@ -31,6 +31,7 @@
"@stefanprobst/rehype-extract-toc": "^2.2.0",
"@turf/turf": "^7.1.0",
"@types/mdx": "^2.0.8",
"@types/react-native": "^0.73.0",
"@types/topojson-client": "^3.1.5",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
@@ -57,8 +58,8 @@
"next": "15.2.1",
"next-themes": "^0.2.1",
"qrcode": "^1.5.4",
"react": "^18",
"react-dom": "^18",
"react": "catalog:",
"react-dom": "catalog:",
"react-singleton-hook": "^4.0.1",
"shiki": "^3.2.1",
"tailwind-merge": "^1.14.0",
@@ -70,8 +71,8 @@
"@playwright/test": "^1.52.0",
"@types/geojson": "^7946.0.14",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"autoprefixer": "^10",
"pagefind": "^1.3.0",
"postcss": "^8",

View File

@@ -16,5 +16,6 @@
"persistent": true,
"dependsOn": ["build"]
}
}
},
"extends": ["//"]
}

4169
homepage/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,10 @@
packages:
- "homepage"
- "design-system"
- "gcmp"
- "gcmp"
catalog:
"react": "19.0.0"
"react-dom": "19.0.0"
"@types/react": "19.0.0"
"@types/react-dom": "19.0.0"

View File

@@ -55,7 +55,6 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"esbuild": "0.24.0"
},
"patchedDependencies": {
"expo-router": "patches/expo-router.patch"

View File

@@ -1,5 +1,12 @@
# cojson-storage-indexeddb
## 0.14.24
### Patch Changes
- cojson@0.14.24
- cojson-storage@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage-indexeddb",
"version": "0.14.23",
"version": "0.14.24",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,12 @@
# cojson-storage-sqlite
## 0.14.24
### Patch Changes
- cojson@0.14.24
- cojson-storage@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "cojson-storage-sqlite",
"type": "module",
"version": "0.14.23",
"version": "0.14.24",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.7.0",
"cojson": "workspace:0.14.23",
"cojson": "workspace:0.14.24",
"cojson-storage": "workspace:*"
},
"devDependencies": {

View File

@@ -1,5 +1,11 @@
# cojson-storage
## 0.14.24
### Patch Changes
- cojson@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "cojson-storage",
"version": "0.14.23",
"version": "0.14.24",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",

View File

@@ -1,5 +1,11 @@
# cojson-transport-nodejs-ws
## 0.14.24
### Patch Changes
- cojson@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "cojson-transport-ws",
"type": "module",
"version": "0.14.23",
"version": "0.14.24",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -1,5 +1,7 @@
# cojson
## 0.14.24
## 0.14.23
### Patch Changes

View File

@@ -25,7 +25,7 @@
},
"type": "module",
"license": "MIT",
"version": "0.14.23",
"version": "0.14.24",
"devDependencies": {
"@opentelemetry/sdk-metrics": "^2.0.0",
"typescript": "catalog:"

View File

@@ -369,11 +369,9 @@ export class CoValueCore {
tryAddTransactions(
sessionID: SessionID,
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
notifyMode: "immediate" | "deferred",
skipVerify: boolean = false,
givenNewStreamingHash?: StreamingHash,
): Result<true, TryAddTransactionsError> {
return this.node
.resolveAccountAgent(
@@ -390,14 +388,12 @@ export class CoValueCore {
const signerID = this.crypto.getAgentSignerID(agent);
const result = this.verified.tryAddTransactions(
const result = this.verified.tryAdd(
sessionID,
signerID,
newTransactions,
givenExpectedNewHash,
newSignature,
skipVerify,
givenNewStreamingHash,
);
if (result.isOk()) {
@@ -531,30 +527,33 @@ export class CoValueCore {
) as SessionID)
: this.node.currentSessionID;
const { expectedNewHash, newStreamingHash } =
this.verified.expectedNewHashAfter(sessionID, [transaction]);
const agent = this.node.getCurrentAgent();
const signature = this.crypto.sign(
this.node.getCurrentAgent().currentSignerSecret(),
expectedNewHash,
this.verified.addNew(
sessionID,
agent.currentSignerID(),
[transaction],
agent.currentSignerSecret(),
);
const success = this.tryAddTransactions(
sessionID,
[transaction],
expectedNewHash,
signature,
"immediate",
true,
newStreamingHash,
)._unsafeUnwrap({ withStackTrace: true });
this.node.syncManager.recordTransactionsSize([transaction], "local");
void this.node.syncManager.requestCoValueSync(this);
if (success) {
this.node.syncManager.recordTransactionsSize([transaction], "local");
void this.node.syncManager.requestCoValueSync(this);
if (
this._cachedContent &&
"processNewTransactions" in this._cachedContent &&
typeof this._cachedContent.processNewTransactions === "function"
) {
this._cachedContent.processNewTransactions();
} else {
this._cachedContent = undefined;
}
return success;
this._cachedDependentOn = undefined;
this.notifyUpdate("immediate");
return true;
}
getCurrentContent(options?: {

View File

@@ -1,12 +1,16 @@
import { Result, err, ok } from "neverthrow";
import { AnyRawCoValue } from "../coValue.js";
import {
APPEND_INVALID_SIGNATURE,
APPEND_OK,
AppendOnlyVerifiedLog,
CryptoProvider,
Encrypted,
Hash,
KeyID,
Signature,
SignerID,
SignerSecret,
StreamingHash,
} from "../crypto/crypto.js";
import { RawCoID, SessionID, TransactionID } from "../ids.js";
@@ -48,13 +52,15 @@ export type TrustingTransaction = {
export type Transaction = PrivateTransaction | TrustingTransaction;
type SessionLog = {
readonly transactions: Transaction[];
lastHash?: Hash;
streamingHash: StreamingHash;
readonly signatureAfter: { [txIdx: number]: Signature | undefined };
lastSignature: Signature;
};
// type SessionLog = {
// readonly transactions: Transaction[];
// lastHash?: Hash;
// streamingHash: StreamingHash;
// readonly signatureAfter: { [txIdx: number]: Signature | undefined };
// lastSignature: Signature;
// };
type SessionLog = AppendOnlyVerifiedLog<Transaction>;
export type ValidatedSessions = Map<SessionID, SessionLog>;
@@ -82,134 +88,47 @@ export class VerifiedState {
// do a deep clone, including the sessions
const clonedSessions = new Map();
for (let [sessionID, sessionLog] of this.sessions) {
clonedSessions.set(sessionID, {
lastSignature: sessionLog.lastSignature,
lastHash: sessionLog.lastHash,
streamingHash: sessionLog.streamingHash.clone(),
signatureAfter: { ...sessionLog.signatureAfter },
transactions: sessionLog.transactions.slice(),
} satisfies SessionLog);
clonedSessions.set(sessionID, sessionLog.clone());
}
return new VerifiedState(this.id, this.crypto, this.header, clonedSessions);
}
tryAddTransactions(
addNew(
sessionID: SessionID,
signerID: SignerID,
newTransactions: Transaction[],
signerSecret: SignerSecret,
) {
const sessionLog =
this.sessions.get(sessionID) ||
this.crypto.emptyAppendOnlyVerifiedLog(signerID);
sessionLog.addNew(newTransactions, signerSecret);
this.sessions.set(sessionID, sessionLog);
}
tryAdd(
sessionID: SessionID,
signerID: SignerID,
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
skipVerify: boolean = false,
givenNewStreamingHash?: StreamingHash,
): Result<true, TryAddTransactionsError> {
if (skipVerify === true && givenNewStreamingHash && givenExpectedNewHash) {
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
givenExpectedNewHash,
givenNewStreamingHash,
);
const sessionLog =
this.sessions.get(sessionID) ||
this.crypto.emptyAppendOnlyVerifiedLog(signerID);
const result = sessionLog.tryAdd(newTransactions, newSignature, skipVerify);
if (result === APPEND_OK) {
this.sessions.set(sessionID, sessionLog);
return ok(true as const);
} else {
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
sessionID,
newTransactions,
);
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
givenExpectedNewHash,
} satisfies InvalidHashError);
}
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
this.doAddTransactions(
sessionID,
newTransactions,
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
expectedNewHash,
newStreamingHash,
);
sessionID,
signerID: sessionLog.signerID,
} satisfies InvalidSignatureError);
}
return ok(true as const);
}
private doAddTransactions(
sessionID: SessionID,
newTransactions: Transaction[],
newSignature: Signature,
expectedNewHash: Hash,
newStreamingHash: StreamingHash,
) {
const transactions = this.sessions.get(sessionID)?.transactions ?? [];
for (const tx of newTransactions) {
transactions.push(tx);
}
const signatureAfter = this.sessions.get(sessionID)?.signatureAfter ?? {};
const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
(max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
-1,
);
const sizeOfTxsSinceLastInbetweenSignature = transactions
.slice(lastInbetweenSignatureIdx + 1)
.reduce(
(sum, tx) =>
sum +
(tx.privacy === "private"
? tx.encryptedChanges.length
: tx.changes.length),
0,
);
if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
signatureAfter[transactions.length - 1] = newSignature;
}
this.sessions.set(sessionID, {
transactions,
lastHash: expectedNewHash,
streamingHash: newStreamingHash,
lastSignature: newSignature,
signatureAfter: signatureAfter,
});
this._cachedNewContentSinceEmpty = undefined;
this._cachedKnownState = undefined;
}
expectedNewHashAfter(
sessionID: SessionID,
newTransactions: Transaction[],
): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
const streamingHash =
this.sessions.get(sessionID)?.streamingHash.clone() ??
new StreamingHash(this.crypto);
for (const transaction of newTransactions) {
streamingHash.update(transaction);
}
return {
expectedNewHash: streamingHash.digest(),
newStreamingHash: streamingHash,
};
}
newContentSince(

View File

@@ -1,4 +1,5 @@
import { base58 } from "@scure/base";
import { Transaction } from "../coValueCore/verifiedState.js";
import { RawAccountID } from "../coValues/account.js";
import { AgentID, RawCoID, TransactionID } from "../ids.js";
import { SessionID } from "../ids.js";
@@ -297,6 +298,10 @@ export abstract class CryptoProvider<Blake3State = any> {
newRandomSessionID(accountID: RawAccountID | AgentID): SessionID {
return `${accountID}_session_z${base58.encode(this.randomBytes(8))}`;
}
abstract emptyAppendOnlyVerifiedLog<T>(
signedID: SignerID,
): AppendOnlyVerifiedLog<T>;
}
export type Hash = `hash_z${string}`;
@@ -341,3 +346,34 @@ export type KeySecret = `keySecret_z${string}`;
export type KeyID = `key_z${string}`;
export const secretSeedLength = 32;
export const APPEND_OK = 0;
export const APPEND_INVALID_SIGNATURE = 1;
export type AppendResult = typeof APPEND_OK | typeof APPEND_INVALID_SIGNATURE;
export interface SessionLog {
// these only have to be maintained in JS, anything lower level
// only needs to store the encoded items and last streaming hash
// and return the new signature on addNew
transactions: readonly Transaction[];
signerID: SignerID;
lastSignature: Signature;
signatureAfter: { [txIdx: number]: Signature | undefined };
clone(): SessionLog;
tryAdd(
transactions: Transaction[],
newSignature: Signature,
skipVerify: boolean,
): AppendResult;
addNewTransaction(changes: JsonValue[], signerSecret: SignerSecret): void;
// note: this may need to decrypt all transactions since the last decrypted one, even if some of these are invalid
// in case we use compression etc.
// invariant: we must call this with strictly monotonically increasing txIndex
decryptNextTransactionChanges(
txIndex: number,
keySecret: KeySecret,
): JsonValue[] | undefined;
}

View File

@@ -1,5 +1,11 @@
# create-jazz-app
## 0.1.22
### Patch Changes
- 5bb1252: delete .env.test file
## 0.1.21
### Patch Changes

View File

@@ -5,7 +5,7 @@
"types": "src/index.ts",
"type": "module",
"license": "MIT",
"version": "0.1.21",
"version": "0.1.22",
"bin": {
"create-jazz-app": "./dist/index.js"
},

View File

@@ -105,7 +105,7 @@ export const frameworkToAuthExamples: Partial<
},
"svelte-passkey-auth": {
name: "Passkey auth",
repo: "garden-co/jazz/examples/passkey-svelte",
repo: "garden-co/jazz/starters/svelte-passkey-auth",
platform: PLATFORM.WEB,
},
"rn-minimal-auth": {

View File

@@ -147,6 +147,13 @@ async function scaffoldProject({
verbose: true,
});
await emitter.clone(projectName);
// Remove .env.test file if it exists
const envTestFilePath = `${projectName}/.env.test`;
if (fs.existsSync(envTestFilePath)) {
fs.unlinkSync(envTestFilePath);
}
cloneSpinner.succeed(chalk.green("Template cloned successfully"));
} catch (error) {
cloneSpinner.fail(chalk.red("Failed to clone template"));

View File

@@ -1,5 +1,14 @@
# jazz-auth-betterauth
## 0.14.24
### Patch Changes
- cojson@0.14.24
- jazz-browser@0.14.24
- jazz-betterauth-client-plugin@0.14.24
- jazz-tools@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-auth-betterauth",
"version": "0.14.23",
"version": "0.14.24",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,13 @@
# jazz-auth-clerk
## 0.14.24
### Patch Changes
- cojson@0.14.24
- jazz-browser@0.14.24
- jazz-tools@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,14 +1,14 @@
{
"name": "jazz-auth-clerk",
"version": "0.14.23",
"version": "0.14.24",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"dependencies": {
"cojson": "workspace:0.14.23",
"jazz-browser": "workspace:0.14.23",
"jazz-tools": "workspace:0.14.23"
"cojson": "workspace:0.14.24",
"jazz-browser": "workspace:0.14.24",
"jazz-tools": "workspace:0.14.24"
},
"scripts": {
"format-and-lint": "biome check .",

View File

@@ -1,5 +1,11 @@
# jazz-betterauth-client-plugin
## 0.14.24
### Patch Changes
- jazz-betterauth-server-plugin@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-client-plugin",
"version": "0.14.23",
"version": "0.14.24",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,13 @@
# jazz-betterauth-server-plugin
## 0.14.24
### Patch Changes
- cojson@0.14.24
- jazz-browser@0.14.24
- jazz-tools@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-betterauth-server-plugin",
"version": "0.14.23",
"version": "0.14.24",
"type": "module",
"main": "dist/index.js",
"types": "src/index.ts",

View File

@@ -1,5 +1,12 @@
# jazz-browser-media-images
## 0.14.24
### Patch Changes
- jazz-browser@0.14.24
- jazz-tools@0.14.24
## 0.14.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "jazz-browser-media-images",
"version": "0.14.23",
"version": "0.14.24",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -8,8 +8,8 @@
"dependencies": {
"@types/image-blob-reduce": "^4.1.1",
"image-blob-reduce": "^4.1.0",
"jazz-browser": "workspace:0.14.23",
"jazz-tools": "workspace:0.14.23",
"jazz-browser": "workspace:0.14.24",
"jazz-tools": "workspace:0.14.24",
"pica": "^9.0.1"
},
"scripts": {

Some files were not shown because too many files have changed in this diff Show More