jazz-vue-vamp
v0.15.0
Published
Vue 3 composition API bindings for [Jazz.Tools](https://jazz.tools) - a framework for local-first collaborative applications.
Readme
jazz-vue-vamp
Vue 3 composition API bindings for Jazz.Tools - a framework for local-first collaborative applications.
Vamp?
Vamp is a chord progression in Jazz that extends a song's duration. The whole point of this package is to extend Jazz.Tools into the Vue ecosystem, so it seemed like a good name. And we can't just call it jazz-vue, because that namespace was used back when Jazz was maintaining their own Vue package.
Features
- 🎵 Vue 3 Composition API - Built for modern Vue with
<script setup>support - 🔄 Real-time sync - Collaborative state that updates across all connected clients
- 💾 Offline-first - Works seamlessly offline with automatic sync when reconnected
- 🔐 End-to-end encrypted - Your data is encrypted and signed on-device
- 🚀 No backend required - Jazz handles sync, storage, and permissions
- 🎯 Type-safe - Full TypeScript support with schema validation
Installation
npm install jazz-vue-vamp jazz-toolsQuick Start
1. Define Your Schema
// schema.ts
import { co, z } from "jazz-tools";
const TodoItem = co.map({
title: z.string(),
completed: z.boolean(),
});
const TodoList = co.list(TodoItem);
const AccountRoot = co.map({
todos: TodoList,
});
export const MyAppAccount = co.account({
root: AccountRoot,
profile: co.map({
name: z.string()
}),
});2. Set Up the Provider
<!-- App.vue -->
<template>
<JazzVueProvider
:sync="{ peer: 'wss://cloud.jazz.tools/[email protected]' }"
:AccountSchema="MyAppAccount"
>
<TodoApp />
</JazzVueProvider>
</template>
<script setup lang="ts">
import { JazzVueProvider } from "jazz-vue-vamp";
import { MyAppAccount } from "./schema";
import TodoApp from "./TodoApp.vue";
</script>3. Use Jazz Composables
<!-- TodoApp.vue -->
<template>
<div>
<h1>My Todos</h1>
<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="Add a todo..." />
<button type="submit">Add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo)"
/>
{{ todo.title }}
</li>
</ul>
<button @click="logOut">Sign Out</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useAccount } from "jazz-vue-vamp";
import { MyAppAccount, TodoItem } from "./schema";
// Get the current user with resolved data
const { me, agent, logOut } = useAccount(MyAppAccount, {
resolve: { root: { todos: true } }
});
const newTodo = ref("");
// Reactive access to todos
const todos = computed(() => me.value?.root?.todos || []);
function addTodo() {
if (!newTodo.value.trim() || !me.value?.root?.todos) return;
const todo = TodoItem.create({
title: newTodo.value,
completed: false
}, { owner: agent.value }); // Use agent for ownership
me.value.root.todos.push(todo);
newTodo.value = "";
}
function toggleTodo(todo: any) {
todo.completed = !todo.completed;
}
</script>Core Composables
useAccount()
Access the current user account and agent. Works in all authentication modes.
Return Value:
me- The authenticated user account (always nullable)agent- Current agent (authenticated account or anonymous guest)logOut- Function to sign out
Authentication States
The useAccount() composable handles three distinct authentication states:
Authenticated Users (auth mode with valid credentials):
me.value- Has the user's persistent accountagent.value._type- Returns"Account"agent.value- Same asme.value(the authenticated account)
Unauthenticated Users (auth mode without valid credentials):
me.value- Has a temporary account for the sessionagent.value._type- Returns"Anonymous"agent.value- The temporary account (can be used to load public data)
Guest Mode Users (
guestMode: truein provider):me.value- Alwaysnull(no persistent account)agent.value._type- Returns"Anonymous"agent.value- Anonymous agent (can be used to load public data)
// Basic usage - works for authenticated and guest users
const { me, agent, logOut } = useAccount();
// Handle different authentication states
if (agent.value._type === "Account") {
// Authenticated user
console.log("Welcome back!", me.value?.profile?.name);
// Use me.value or agent.value (they're the same)
} else {
// Anonymous user (either unauthenticated or guest mode)
if (me.value) {
// Unauthenticated user with temporary account
console.log("You have a temporary session");
// Can use me.value for temporary data
} else {
// True guest mode
console.log("You're browsing as a guest");
// me.value is null, use agent.value for public data only
}
}
// With schema (provides better typing)
const { me, agent, logOut } = useAccount(MyAppAccount);
// With deep resolution for authenticated users
const { me, agent, logOut } = useAccount(MyAppAccount, {
resolve: {
root: { todos: true },
profile: true
}
});
// Load public data (works in all modes)
const publicTodos = useCoState(TodoList, publicTodoListId.value, {
loadAs: agent.value // Always available regardless of auth state
});
// Load private data (only works when me.value exists)
const privateTodos = computed(() => {
if (!me.value) return null;
return useCoState(TodoList, me.value.root?.privateTodosId, {
loadAs: agent.value
});
});useCoState()
Subscribe to any CoValue by ID with automatic updates.
const todo = useCoState(TodoItem, todoId, {
resolve: { assignee: true }
});
// todo.value updates automatically when the data changesuseAcceptInvite()
Handle collaborative invites in your app.
useAcceptInvite({
invitedObjectSchema: TodoList,
onAccept: (todoListId) => {
// Navigate to the shared todo list
router.push(`/todos/${todoListId}`);
}
});experimental_useInboxSender()
Send messages to other users' inboxes for real-time communication.
// Send a message to another user
const sendMessage = experimental_useInboxSender(recipientUserId);
// Send structured data
await sendMessage({
type: 'notification',
content: 'You have a new todo!',
todoId: 'todo_123'
});
// Works with reactive recipient IDs
const selectedUser = ref('user_456');
const sendToSelected = experimental_useInboxSender(selectedUser);useIsAuthenticated()
Check if the current user is authenticated.
const isAuthenticated = useIsAuthenticated();
// Use in template
// <div v-if="isAuthenticated">Welcome back!</div>
// <AuthForm v-else />
// Or watch for changes
watchEffect(() => {
if (isAuthenticated.value) {
// User just signed in
router.push('/dashboard');
}
});useJazzContext()
Access the raw Jazz context for advanced use cases.
const context = useJazzContext();
// Access node, networking, etc.JazzVueProvider
The JazzVueProvider component sets up the Jazz context for your entire application.
Basic Usage
<JazzVueProvider
:sync="{ peer: 'wss://cloud.jazz.tools/[email protected]' }"
:AccountSchema="MyAppAccount"
>
<MyApp />
</JazzVueProvider>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| sync | SyncConfig | required | Sync configuration with peer URL |
| AccountSchema | AccountClass \| AnyAccountSchema | Account | Account schema for your app (supports both class-based AccountClass and schema-based co.account() accounts) |
| guestMode | boolean | false | Allow anonymous/guest access |
| storage | "indexedDB" | undefined | Storage backend (indexedDB recommended) |
| defaultProfileName | string | undefined | Default name for new user profiles |
| enableSSR | boolean | false | Enable server-side rendering support |
| logOutReplacement | () => void | undefined | Custom logout handler |
| onLogOut | () => void | undefined | Callback when user logs out |
| onAnonymousAccountDiscarded | (account) => Promise<void> | undefined | Handle anonymous account cleanup |
Advanced Configuration
<JazzVueProvider
:sync="{
peer: 'wss://cloud.jazz.tools/[email protected]',
when: 'online'
}"
:AccountSchema="MyAppAccount"
:enableSSR="true"
storage="indexedDB"
defaultProfileName="Anonymous User"
:onLogOut="handleLogOut"
:logOutReplacement="customLogOut"
:onAnonymousAccountDiscarded="cleanupAnonymousData"
>
<MyApp />
</JazzVueProvider>Authentication
Jazz supports multiple authentication methods:
Passkey Authentication
<template>
<div>
<DemoAuthBasicUI />
</div>
</template>
<script setup lang="ts">
import { DemoAuthBasicUI } from "jazz-vue-vamp/auth";
</script>Custom Authentication
import { usePasskeyAuth, usePassphraseAuth } from "jazz-vue-vamp/auth";
// Passkey auth (recommended)
const { signUp, signIn } = usePasskeyAuth();
// Passphrase auth
const { signUp, signIn } = usePassphraseAuth();Sharing and Permissions
Create collaborative features with Groups:
import { Group } from "jazz-tools";
// Create a group for collaboration
const group = Group.create({ owner: me.value });
group.addMember(friend, "writer");
// Create shared data
const sharedTodos = TodoList.create([], { owner: group });
// Create invite links
import { createInviteLink } from "jazz-vue-vamp";
const inviteLink = createInviteLink(sharedTodos, "writer");Advanced Usage
TypeScript Support
The package provides full TypeScript support. For the best experience, augment the account type:
// For traditional class-based schemas
declare module "jazz-vue-vamp/provider" {
interface Register {
Account: MyAppAccount;
}
}
// For new co.account() schemas, type inference works automaticallyDeep Loading
Control what data is loaded and when:
// Load specific references
const { me, agent } = useAccount(MyAppAccount, {
resolve: {
root: {
todos: {
assignee: true // Load assignee details for each todo
}
}
}
});
// Conditional loading
const { me, agent } = useAccount(MyAppAccount, {
resolve: selectedTodoId.value ? {
root: { todos: { [selectedTodoId.value]: true } }
} : {}
});Guest Mode
Allow anonymous users to access public data:
<JazzVueProvider
:guestMode="true"
:sync="{ peer: 'wss://cloud.jazz.tools' }"
>
<PublicTodosView />
</JazzVueProvider>Testing
The package includes utilities for testing:
import { withJazzTestSetup, createJazzTestAccount } from "jazz-vue-vamp/testing";
describe("Todo functionality", () => {
it("should create todos", async () => {
const account = await createJazzTestAccount({
AccountSchema: MyAppAccount
});
const [result] = withJazzTestSetup(
() => useAccount(MyAppAccount),
{ account }
);
expect(result.agent.value).toBeDefined();
expect(result.me.value).toBeDefined(); // May be null initially
});
});Troubleshooting
"No active account" Error During Account Creation
If you encounter this error during account setup, it's likely due to schema migrations trying to create CoValues without explicit groups. During migration, the active account context isn't available yet.
❌ Problem:
export const MyAccount = co.account({
root: MyRoot,
}).withMigration(async (account) => {
if (!account.root) {
// This will fail - no group specified
account.root = MyRoot.create({ items: [] });
}
});✅ Solution:
import { Group } from "jazz-tools";
export const MyAccount = co.account({
root: MyRoot,
}).withMigration(async (account) => {
if (!account.root) {
// Explicitly create and pass the group
const group = Group.create(account);
account.root = MyRoot.create({ items: [] }, group);
}
if (!account.profile) {
const profileGroup = Group.create(account);
profileGroup.makePublic(); // If needed
account.profile = MyProfile.create({ name: "User" }, profileGroup);
}
});Key Points:
- All CoValue creation during migrations needs explicit groups
- Pass
Group.create(account)as the second parameter to.create() - This includes Maps, Lists, and any nested CoValues
- The account object is available in the migration function but not in the active context yet
TypeScript Errors with New Account Schemas
If you're using the new co.account() syntax and getting type errors:
// Make sure to import the account schema type
import type { MyAppAccount } from "./schema";
// Use the schema with useAccount for better typing
const { me, agent } = useAccount(MyAppAccount, {
resolve: { root: { todos: true } }
});Known Issues
co.account() Schema Support (jazz-tools 0.15.4)
Due to compatibility issues with jazz-tools 0.15.4, co.account() schemas temporarily fall back to the default Account class. This means:
- Class-based account schemas: Work normally ✅
co.account()schemas: Fall back to default Account class ⚠️
// This works normally
const { me, agent } = useAccount(MyAccountClass);
// This falls back to default Account class until fixed
const { me, agent } = useAccount(MyAccountSchema); // co.account() schemaWe're tracking this issue and will restore full co.account() support in a future release once the upstream compatibility issue is resolved.
Examples
Check out these examples to see jazz-vue-vamp in action:
- Todo App - Basic collaborative todo list
- Chat App - Real-time messaging
- Drawing Canvas - Collaborative drawing with presence
- Document Editor - Rich text collaboration
Migration from jazz-vue
If you're migrating from the original jazz-vue package:
- Update imports:
jazz-vue→jazz-vue-vamp - The API is largely compatible, but check the composable signatures
- New
co.account()syntax is now supported alongside class-based schemas
Migration from pre-0.15.0 versions
If you're upgrading from earlier versions of vamp or jazz-vue, note these changes in 0.15.0:
Package Updates
# Update to jazz-tools 0.15.4
npm install jazz-vue-vamp@^0.15.0 jazz-tools@^0.15.4
# Remove jazz-browser (now included in jazz-tools)
npm uninstall jazz-browserImport Changes
// Before (if using jazz-browser directly)
import { createInviteLink, parseInviteLink } from "jazz-browser";
// After (jazz-browser functionality moved to jazz-tools/browser)
import { createInviteLink, parseInviteLink } from "jazz-tools/browser";Component Rename
<!-- Before (pre-0.15.0) -->
<template>
<JazzProvider :sync="sync" :AccountSchema="MyAccount">
<MyApp />
</JazzProvider>
</template>
<script setup>
import { JazzProvider } from "jazz-vue-vamp";
</script>
<!-- After (0.15.0) -->
<template>
<JazzVueProvider :sync="sync" :AccountSchema="MyAccount">
<MyApp />
</JazzVueProvider>
</template>
<script setup>
import { JazzVueProvider } from "jazz-vue-vamp";
</script>Note: Starting from 0.15.0, you must use JazzVueProvider - the old JazzProvider name is no longer available.
useAccount() Changes
// Before (pre-0.15.0)
const { me, logOut } = useAccount();
// me was never null (auto-fallback to contextMe)
// After (0.15.0)
const { me, agent, logOut } = useAccount();
// me is now always nullable, agent is always availableuseAccountOrGuest() Removed
// Before (pre-0.15.0)
const { me } = useAccountOrGuest();
if (me.value._type === "Anonymous") {
// Handle guest
} else {
// Handle authenticated user
}
// After (0.15.0) - Use useAccount() instead
const { me, agent } = useAccount();
if (agent.value._type === "Anonymous") {
// Handle guest - me will be null
// Use agent to load data
} else {
// Handle authenticated user - me may be null until loaded
// Use agent to load data
}New Features Available
- experimental_useInboxSender() - Send messages between users
- Enhanced JazzVueProvider - New props for SSR and custom logout handling
- Better error handling - Improved account creation and subscription management
- Performance improvements - Optimized context management and subscription cleanup
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
MIT - see LICENSE.txt for details.
Related
- Jazz.Tools - Main Jazz framework
- jazz-tools - Core Jazz package
- jazz-react - React bindings
