Conversation
- Add Jazz schema (lib/schema.ts) with CoValue definitions for Todo, Sublist, TodoList, and Account - Add Jazz provider with passphrase authentication (lib/jazz.tsx) - Remove InstantDB dependencies and files (db.ts, transactions.ts, types.ts, instant.schema.ts, instant.perms.ts, instantdb.txt) - Remove debug CLI tool (debug-cli.js, debug-examples.sh) - Update dependencies: remove @instantdb/react, add jazz-react, jazz-tools, @scure/bip39 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update layout.tsx to use JazzProvider - Rewrite page.tsx with Jazz hooks, profile settings modal, and invite URL persistence - Rewrite TodoListView.tsx with Jazz CoValue operations, drag-and-drop, and share modal with read/write invite links - Simplify InvitationsView.tsx for Jazz invite acceptance flow - Update HashRouter for Jazz CoValue IDs - Fix textarea styling for passphrase visibility in globals.css - Update utils.ts to remove slug generation (now using Jazz IDs) - Update LLM.md documentation Features: - Passphrase-based authentication - Profile settings (display name) - Read-only and write access invite links - Member list display in share modal - Warning about permanent invite link access 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the Smart Todos application from InstantDB to Jazz, introducing end-to-end encryption and fundamentally changing the authentication, data model, and sharing mechanisms.
Purpose: Replace InstantDB backend with Jazz to enable end-to-end encryption and local-first collaboration.
Key Changes:
- Authentication switched from magic link (email + code) to passphrase-based recovery phrases
- Data model migrated from InstantDB schema to Jazz CoValues with nested structures
- Sharing changed from email-based invitations to cryptographic invite links
- Direct property mutation replaces transaction-based updates
Reviewed changes
Copilot reviewed 18 out of 21 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Removed InstantDB dependencies, added jazz-react and jazz-tools; updated Next.js to 16.1.1 |
| tsconfig.json | Changed JSX transform to "react-jsx" for automatic JSX runtime |
| lib/schema.ts | New Jazz CoValue schema with TodoList, Todo, Sublist, and Account structures |
| lib/jazz.tsx | New Jazz provider with passphrase authentication and typed hooks |
| lib/utils.ts | Updated to use Jazz IDs instead of custom slugs |
| lib/db.ts | Deleted - InstantDB initialization removed |
| lib/types.ts | Deleted - types moved to schema |
| lib/transactions.ts | Deleted - direct mutations replace transactions |
| instant.schema.ts | Deleted - replaced by lib/schema.ts |
| instant.perms.ts | Deleted - permissions now handled by Jazz Groups |
| app/page.tsx | Complete rewrite for Jazz authentication and CoValue management |
| app/layout.tsx | Wrapped app in JazzProvider |
| app/components/InvitationsView.tsx | Simplified to handle Jazz invite links via useAcceptInvite hook |
| app/components/HashRouter.tsx | Updated routing to handle Jazz CoValue IDs and invite links |
| debug-cli.js | Deleted - InstantDB debug tooling no longer applicable |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Get API key from environment or use email for development | ||
| const JAZZ_PEER = (process.env.NEXT_PUBLIC_JAZZ_PEER || "wss://cloud.jazz.tools/?key=todo@tionis.dev") as `wss://${string}`; |
There was a problem hiding this comment.
The hardcoded Jazz peer URL includes an email address "todo@tionis.dev". If this is meant to be production code, consider moving this to an environment variable without a default value to prevent unintended usage of someone else's Jazz cloud instance. This could lead to data being stored on the wrong server or potential security issues.
| // Get API key from environment or use email for development | |
| const JAZZ_PEER = (process.env.NEXT_PUBLIC_JAZZ_PEER || "wss://cloud.jazz.tools/?key=todo@tionis.dev") as `wss://${string}`; | |
| // Get Jazz peer URL from environment; no hardcoded default to avoid using someone else's instance | |
| const JAZZ_PEER_ENV = process.env.NEXT_PUBLIC_JAZZ_PEER; | |
| if (!JAZZ_PEER_ENV) { | |
| throw new Error("Environment variable NEXT_PUBLIC_JAZZ_PEER must be set to a valid Jazz peer wss:// URL."); | |
| } | |
| const JAZZ_PEER = JAZZ_PEER_ENV as `wss://${string}`; |
| import { ID } from "jazz-tools"; | ||
|
|
||
| export function getListUrl(slug: string): string { | ||
| export function getListUrl(listId: ID<any>): string { |
There was a problem hiding this comment.
The function signature change from getListUrl(slug: string) to getListUrl(listId: ID<any>) uses a generic ID<any> type instead of a more specific type like ID<TodoList>. This loses type safety. Consider using ID<TodoList> to ensure only valid TodoList IDs are passed to this function.
| export function getListUrl(listId: ID<any>): string { | |
| export function getListUrl<T>(listId: ID<T>): string { |
| sessionStorage.setItem(PENDING_INVITE_KEY, hash); | ||
| } | ||
|
|
||
| // If we just logged in and have a pending invite, restore it | ||
| if (me && me.root) { | ||
| const pendingInvite = sessionStorage.getItem(PENDING_INVITE_KEY); | ||
| if (pendingInvite) { | ||
| sessionStorage.removeItem(PENDING_INVITE_KEY); |
There was a problem hiding this comment.
The use of sessionStorage for storing pending invite links could lead to data loss if the user opens the invite link in a different tab or browser. Consider using localStorage instead for better persistence, or implement a more robust solution that handles cross-tab/cross-browser scenarios.
| sessionStorage.setItem(PENDING_INVITE_KEY, hash); | |
| } | |
| // If we just logged in and have a pending invite, restore it | |
| if (me && me.root) { | |
| const pendingInvite = sessionStorage.getItem(PENDING_INVITE_KEY); | |
| if (pendingInvite) { | |
| sessionStorage.removeItem(PENDING_INVITE_KEY); | |
| localStorage.setItem(PENDING_INVITE_KEY, hash); | |
| } | |
| // If we just logged in and have a pending invite, restore it | |
| if (me && me.root) { | |
| const pendingInvite = localStorage.getItem(PENDING_INVITE_KEY); | |
| if (pendingInvite) { | |
| localStorage.removeItem(PENDING_INVITE_KEY); |
| window.location.hash = `/list/${newList.id}`; | ||
| } catch (err) { | ||
| console.error("Failed to create list:", err); | ||
| setShowErrorModal({show: true, message: "Failed to create list. Please try again."}); |
There was a problem hiding this comment.
The error handling for list creation only logs to console and shows a generic error modal. Users won't see the specific error that occurred. Consider passing the actual error message to the modal to help users understand what went wrong, especially for permission or network-related issues.
| setShowErrorModal({show: true, message: "Failed to create list. Please try again."}); | |
| const errorMessage = err instanceof Error ? err.message : String(err); | |
| setShowErrorModal({ | |
| show: true, | |
| message: `Failed to create list: ${errorMessage}. Please try again.`, | |
| }); |
| "jazz-react": "^0.14.28", | ||
| "jazz-tools": "^0.14.28", | ||
| "next": "^16.1.1", |
There was a problem hiding this comment.
This migration removes all existing user data and lists since there's no migration path from InstantDB to Jazz. Users will lose access to their existing todo lists and will need to re-authenticate. Consider documenting this breaking change prominently in the PR description and release notes, or implementing a data export/import tool to help users transition.
| function App() { | ||
| const [mounted, setMounted] = useState(false); | ||
| const { isLoading, user, error } = db.useAuth(); | ||
| const { me, logOut } = useAccount({ resolve: { root: { todoLists: { $each: true } } } }); |
There was a problem hiding this comment.
Unused variable logOut.
| const { me, logOut } = useAccount({ resolve: { root: { todoLists: { $each: true } } } }); | |
| const { me } = useAccount({ resolve: { root: { todoLists: { $each: true } } } }); |
| KeyboardSensor, | ||
| PointerSensor, | ||
| useSensor, | ||
| import { Group, ID, createInviteLink } from "jazz-tools"; |
There was a problem hiding this comment.
Unused import Group.
| import { Group, ID, createInviteLink } from "jazz-tools"; | |
| import { ID, createInviteLink } from "jazz-tools"; |
| closestCenter, | ||
| KeyboardSensor, | ||
| PointerSensor, | ||
| useSensor, | ||
| useSensors, | ||
| DragOverlay, | ||
| DragStartEvent, | ||
| DragEndEvent, | ||
| DragOverEvent, |
There was a problem hiding this comment.
Unused import DragOverEvent.
| DragOverEvent, |
| import { executeTransaction, canUserWrite, canUserView, transferListOwnership } from "../../lib/transactions"; | ||
| import { useAccount, useCoState } from '../../lib/jazz'; | ||
| import { Account } from "jazz-tools"; | ||
| import { TodoList, Todo, Sublist, ListOfTodos, ListOfSublists } from '../../lib/schema'; |
There was a problem hiding this comment.
Unused imports ListOfSublists, ListOfTodos.
| import { TodoList, Todo, Sublist, ListOfTodos, ListOfSublists } from '../../lib/schema'; | |
| import { TodoList, Todo, Sublist } from '../../lib/schema'; |
| import { useAccount, useCoState } from '../../lib/jazz'; | ||
| import { Account } from "jazz-tools"; | ||
| import { TodoList, Todo, Sublist, ListOfTodos, ListOfSublists } from '../../lib/schema'; | ||
| import { copyToClipboard } from "../../lib/utils"; | ||
| import LoadingSpinner from './LoadingSpinner'; | ||
| import ErrorDisplay from './ErrorDisplay'; |
There was a problem hiding this comment.
Unused import ErrorDisplay.
| import ErrorDisplay from './ErrorDisplay'; |
No description provided.