# Powerhouse Academy - Complete Documentation > Generated: 2026-05-12T12:21:11.234Z > Total Documents: 86 > Source: https://powerhouse.academy/llms-full.txt # Get Started ## Explore the demo package > Source: https://powerhouse.academy/academy/GetStarted/ExploreDemoPackage ## Let's get started To give you a quick idea of how the Powerhouse Vetra builder platform operates on document models and packages, why don't you try installing a package? We will show you how to install the Powerhouse command-line tool `ph-cmd` and then use it to install a pre-built demo package containing a document model, an editor, and a Drive-app. ## Step 1: Install the Powerhouse CLI You will use the Powerhouse CLI to launch a local environment with a "To-do List Demo Package" installed. This is also the package that you'll recreate during the tutorials and gets you familiar with Powerhouse Vetra. ```bash pnpm install -g ph-cmd ``` Verify the installation: ```bash ph --version ``` ## Step 2: Initialize a new project Now use the `ph init` command to initialize a new project and install a Powerhouse package inside the project. You'll be asked to name your project. Afterwards, move inside your project with `cd project-name` ## Step 3: Install the to-do list demo package Now, use the `ph install` command to install the demo package inside the project. ```bash # Install the package ph install @powerhousedao/todo-demo ``` This command downloads and sets up the `@powerhousedao/todo-demo`, making its features available in your Powerhouse project.
Expected CLI result ```bash installing dependencies ๐Ÿ“ฆ ... โ€‰WARNโ€‰ 19 deprecated subdependencies found: @esbuild-kit/core-utils@3.3.2, @esbuild-kit/esm-loader@2.6.5, @npmcli/move-file@1.1.2, @paulmillr/qr@0.2.1, are-we-there-yet@3.0.1, gauge@4.0.4, glob@7.2.3, graphql-language-service-interface@2.10.2, graphql-language-service-parser@1.10.4, graphql-language-service-types@1.8.7, graphql-language-service-utils@2.7.1, inflight@1.0.6, multibase@4.0.6, multicodec@3.2.1, node-domexception@1.0.0, npmlog@6.0.2, rimraf@2.7.1, rimraf@3.0.2, sudo-prompt@8.2.5 Packages: +1 + Progress: resolved 2277, reused 2106, downloaded 1, added 1, done โ€‰WARNโ€‰ Issues with peer dependencies found . โ”œโ”€โ”ฌ @powerhousedao/reactor-browser 3.1.0 โ”‚ โ””โ”€โ”ฌ @powerhousedao/analytics-engine-browser 0.6.0 โ”‚ โ””โ”€โ”€ โœ• unmet peer @powerhousedao/analytics-engine-knex@0.5.1: found 0.6.0 โ”œโ”€โ”ฌ @types/react-dom 19.1.6 โ”‚ โ””โ”€โ”€ โœ• unmet peer @types/react@^19.0.0: found 18.3.23 โ””โ”€โ”ฌ react-native 0.80.0 โ”œโ”€โ”€ โœ• unmet peer @types/react@^19.1.0: found 18.3.23 โ”œโ”€โ”€ โœ• unmet peer react@^19.1.0: found 18.3.1 โ””โ”€โ”ฌ @react-native/virtualized-lists 0.80.0 โ””โ”€โ”€ โœ• unmet peer @types/react@^19.0.0: found 18.3.23 dependencies: + @powerhousedao/todo-demo-package 1.1.1 โ•ญ Warning โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ Ignored build scripts: @acaldas/graphql-codegen-typescript-validation-schema, @apollo/protobufjs, โ”‚ โ”‚ @datadog/pprof, @ipshipyard/node-datachannel, @parcel/watcher, @prisma/client, @prisma/engines, โ”‚ โ”‚ @tailwindcss/oxide, bufferutil, esbuild, keccak, prisma, sqlite3, utf-8-validate. โ”‚ โ”‚ Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ Done in 6s using pnpm v10.9.0 Dependency installed successfully ๐ŸŽ‰ โš™๏ธ Updating powerhouse config file... Config file updated successfully ๐ŸŽ‰ ```
You have now successfully installed `ph-cmd`, initalized a product project and added your first package! ## Step 4: Run the Connect host application To run the package locally in Connect, run: ```bash ph connect ``` Click the returned localhost URL and you should see Connect appear in your browser. **INFO:** **Connect** is the Powerhouse host applicationโ€”a container that runs all your apps, editors, and drives. Think of it as the browser for your Powerhouse ecosystem. **Vetra Studio** (which you'll use for building) runs inside Connect, just like how a web app runs inside a browser.
Connect Home
The Powerhouse Connect interface.
When you click the settings wheel in the bottom right corner, you'll get access to the **Package Manager**. Here, you'll see that you've installed the `@powerhousedao/todo-demo`, which contains not only a **Document Model** and its accompanying editor but also a **Drive-app** specific to the todo-list document model.
Package Manager
The Package Manager showing the installed todo-demo-package.
## Step 5: Create a todo list document **TIP:** A **drive** is a folder to store and organize your documents in. Powerhouse offers the ability to build customized Drive-apps for your documents. Think of a Drive-app as a specialized lensโ€”it offers **different ways to visualize, organize, and interact with** the data stored within a drive, making it more intuitive and efficient for specific use cases. To learn more, visit [Building a Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) **TIP:** An **editor** is a UI component for viewing and modifying a single document. A **Drive-app** is a custom interface for managing multiple documents within a driveโ€”providing aggregated views, progress tracking, and specialized workflows across your document collection. ### 5.1 Create a local todo-list app drive First, let's create a dedicated drive for your to-do lists: - Click the new drive icon in the interface - In the **Drive-app** field, select 'todo-list Drive-app' - This creates a specialized drive that's optimized for to-do list documents ### 5.2 Create a todo-list document Now move into the drive you've just created: - Click the button at the bottom of the page to create a new to-do list document - This opens the to-do list editor where you can start managing your tasks ### 5.3 Add a few todos and inspect the document history - Add a few to-dos that are on your mind - You'll see a statistics widget that counts the open to-dos - After closing the document, look at the todo-list Drive-app interfaceโ€”you'll see that it tracks your tasks and displays a progress bar This is an example of the **usefulness and impact of Drive-apps**. They offer a customized interface that works well with the different documents inside your drive. Read more about Drive-apps in the Mastery Track: [Drive-apps](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer). A key feature of Connect is the **Operations History**. Every change to a document is stored as an individual operation, creating an immutable and replayable history. This provides complete auditability and transparency, as you can inspect each revision, its details, and any associated signatures. For example, you can see a chronological list of all modifications, along with who made them and when.
Operations History Button
You can find the button to visit the operations history in the document model toolbar
Operations History
Example of the operations history for a document, showing all modifications made to it in a list.{" "}
Learn more about the [Operations History](../docs/MasteryTrack/BuildingUserExperiences/DocumentTools/OperationHistory) and other document tools you get for free. ## Step 6: Enable operation signing and verification through Renown Renown is Powerhouse's **decentralized identity and reputation system** designed to address the challenge of trust within open organizations, where contributors often operate under pseudonyms. In traditional organizations, personal identity and reputation are key to establishing trust and accountability. Renown replicates this dynamic in the digital space, allowing contributors to earn experience and build reputation without revealing their real-world identities. **TIP:** When signing in with Renown, use an Ethereum or blockchain address that can function as your 'identity', as this address will accrue more experience and history over time. ### 6.1 Click the renown icon and connect your Ethereum identity "**Log in with Renown**" is a decentralized authentication flow that enables you to log into applications by signing a credential with your Ethereum wallet. Upon signing in, a Decentralized Identifier (DID) is created based on your Ethereum key.
Renown Login
The Renown login screen, prompting for a signature from a wallet.
### 6.2 Authorize Connect to sign document edits on your behalf This DID is then associated with a credential that authorizes a specific Connect instance to act on your behalf. That credential is stored securely on Ceramic, a decentralized data network. When you perform actions through the Powerhouse Connect interface, those operations are signed with the DID and transmitted to Switchboard, which serves as the verifier.
Connect Address for DID
A newly generated DID and address shown within the Connect interface.
Renown Login Complete
Confirmation of a successful login with Renown.
### 6.3 Verify the signatures of new operations in the todo list By leveraging this system, every operation or modification made to a document is cryptographically signed by the contributor's Renown identity. This ensures that each change is verifiable, traceable, and attributable to a specific pseudonymous user, providing a robust audit trail for all document activity. Now, return to your to-do list and make some additional changes. You'll notice that these operations are now signed with your Renown identity, making every action traceable and verifiable in the operations history.
Operation History Signature
Your DID is now signing the operations that are being added to the history.
## Step 7: Export a document Export the document as a `.phd` (Powerhouse Document) file using the export button in the document toolbar at the top. In this toolbar, you will find all available functionality for your documents. The `.phd` file can be sent through any of your preferred channels to other users on your network. ### Up next Now that you have explored a Powerhouse package and discovered its basic functionalities, it is time to start building your own. Our next tutorial focuses on creating a simple to-do list document and will introduce you to the world of **Document Models**โ€”the foundation of **Specification Driven Design & Development**, where structured specs become the shared language between you and AI agents. --- ## Create a new to-do list document > Source: https://powerhouse.academy/academy/GetStarted/CreateNewPowerhouseProject **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-1-initialize-with-ph-init](https://github.com/powerhouse-inc/todo-tutorial/tree/step-1-initialize-with-ph-init) This tutorial step has a corresponding branch in the repository. You can: - View the complete code for this step - Clone and checkout the branch to see the result - Compare your implementation using `git diff` ::: ## Overview This tutorial guides you through creating a simplified version of a 'Powerhouse project' for a **todo-list**. A Powerhouse project primarily consists of a document model and its editor. As your projects use-case expands you can add data-integrations or a specific Drive-app as seen in the demo package. For today's purpose, you'll be using **Vetra Studio**, the builder platform through which developers can access and manage specifications of your project. Vetra Studio runs inside **Connect**, the Powerhouse host application that serves as a container for all Powerhouse apps and drives. ## Prerequisites - Powerhouse CLI installed: `pnpm install -g ph-cmd` or `npm install -g ph-cmd --legacy-peer-deps` - Node.js 24 and a package manager (pnpm or npm) installed - Visual Studio Code (or your preferred IDE) - Terminal/Command Prompt access If you need help with installing the prerequisites you can visit our page [prerequisites](/academy/MasteryTrack/BuilderEnvironment/Prerequisites)
๐Ÿ“– How to use this tutorial This tutorial is designed for you to **build your own project from scratch** while having access to reference code at each step. ### Setup: Create your project and connect to tutorial repo 1. **Create your project** following the tutorial: ```bash mkdir ph-projects cd ph-projects ph init # When prompted, enter project name: todo-tutorial cd todo-tutorial ``` 2. **Add the tutorial repository as a remote** to access reference branches: ```bash git remote add tutorial https://github.com/powerhouse-inc/todo-tutorial.git git fetch tutorial --prune ``` 3. **Create your own branch** to keep your work organized: ```bash git checkout -b my-todo-project ``` Now you have access to all tutorial step branches while working on your own code! ### Compare your work with reference steps At any point, compare what you've built with a tutorial step: ```bash # Compare your current work with step-1 git diff tutorial/step-1-initialize-with-ph-init # See what changed between tutorial steps git diff tutorial/step-1-initialize-with-ph-init..tutorial/step-2-generate-todo-list-document-model # Compare specific files git diff tutorial/step-1-initialize-with-ph-init -- package.json ``` ### Visual diff with GitHub Desktop For a more visual comparison, use GitHub Desktop: 1. **First, make your initial commit** (GitHub Desktop won't show your branch until you have at least one commit): ```bash git add . git commit -m "Initial project setup" ``` 2. **Open GitHub Desktop** and open your repository 3. **Compare branches visually**: - Click on **Branch** menu in the top menu bar - Select **"Compare to Branch..."** - Choose the tutorial branch you want to compare with (e.g., `tutorial/step-1-initialize-with-ph-init`) - GitHub Desktop will show you all file differences in a visual interface 4. **Review the differences**: - Click on any file to see side-by-side or unified diff view - See exactly what's different between your code and the reference **Tip**: You can also use VS Code's Git Graph extension or the command palette โ†’ "Git: Compare with Branch" ### If you get stuck Reset your code to match a tutorial step: ```bash # Reset to step-2 (WARNING: loses your changes) git reset --hard tutorial/step-2-generate-todo-list-document-model ```
## Quick start Create a new Powerhouse project with a single command: ```bash ph init ``` ## Before you begin 1. Open your terminal (either your system terminal or IDE's integrated terminal) 2. Optionally, create a folder first to keep your Powerhouse projects: ```bash mkdir ph-projects cd ph-projects ``` 3. Ensure you're in the correct directory before running the `ph init` command. In the terminal, you will be asked to enter the project name. Fill in the project name and press Enter. ````bash you@yourmachine:~/ph-projects % ph init ? What is the project name? โ€ฃ todo-tutorial ``` ```` Once the project is created, you will see the following output: `bash Initialized empty Git repository in /Users/you/ph-projects/todo-tutorial/.git/ The installation is done! ` Navigate to the newly created project directory: `bash cd todo-tutorial ` ## Develop a single document model in Vetra Studio **Vetra Studio** is the builder's orchestration hub for assembling all specifications needed for your package. It provides a **Vetra Studio Drive** to access, manage, and share document model specifications, editors, and data integrationsโ€”all through a visual interface. For deeper coverage, see the [Vetra Studio documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio). Once in the project directory, run the `ph vetra --watch` command to start a Vetra Studio Drive where you'll be defining your specifications. This is the preferred way to launch your development environment. **INFO:** You'll notice "reactor-api" in the terminal output. A **Reactor** is the Powerhouse back-end service that hosts your drives, handles document synchronization, and provides the GraphQL API. When you run `ph vetra --watch`, a local Reactor starts automatically to power your development environment. ```bash ph vetra --watch ``` The host application for Vetra Studio will start and you will see the following output: ```bash โ„น [reactor-api] [package-manager] Loading packages: @powerhousedao/vetra 14:44:19 โ„น [reactor-api] [server] WebSocket server available at /graphql/subscriptions 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql/system subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql/analytics subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /d/:drive subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql supergraph 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/document-editor subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/vetra-package subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/subgraph-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/processor-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/app-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/vetra-read-model subgraph. 14:44:23 โ„น [reactor-api] [server] MCP server available at http://localhost:4001/mcp 14:44:24 Switchboard initialized 14:44:24 โžœ Drive URL: http://localhost:4001/d/vetra-bac239dd 14:44:24 2:44:24 PM [vite] (client) Re-optimizing dependencies because vite config has changed 14:44:24 Port 3000 is in use, trying another one... 14:44:24 โžœ Local: http://localhost:3000/ 14:44:24 โžœ Network: use --host to expose 14:44:24 โžœ press h + enter to show help ```` A new browser window will open when visiting localhost and you will see the Vetra Studio Drive
Vetra Studio Drive
The Vetra Studio Drive, a builder app that collects all of the specification of a package.
Create a new document model by clicking the Document Models 'Add new specification' button. Name your document TodoList (PascalCase, no spaces or hyphens). If you've followed the steps correctly, you'll have an empty TodoList document where you can define the 'Document Specifications' in the next step.
Alternatively: Develop a single document model in Connect (legacy) **NOTE:** The `ph connect` command is a legacy feature. We recommend using `ph vetra --watch` for all new development, as it provides better tooling and automatic code generation. Once in the project directory, run the `ph connect` command to start a local instance of the Connect application. This allows you to start your document model specification document. Run the following command to start the Connect application: ```bash ph connect ``` The Connect application will start and you will see the following output: ```bash โžœ Local: http://localhost:3000/ โžœ Network: http://192.168.5.110:3000/ โžœ press h + enter to show help ``` A new browser window will open and you will see the Connect application. If it doesn't open automatically, you can open it manually by navigating to `http://localhost:3000/` in your browser. You will see your local drive and a button to create a new drive. **TIP:** If you local drive is not present navigate into Settings in the bottom left corner. Settings > Danger Zone > Clear Storage. Clear the storage of your localhost application as it might has an old session cached. 4. Move into your local drive. Create a new document model by clicking the `DocumentModel` button, found in the 'New Document' section at the bottom of the page. Name your document `TodoList` (PascalCase, no spaces or hyphens). If you've followed the steps correctly, you'll have an empty `TodoList` document where you can define the **'Document Specifications'**.
## Verify your setup At this point, your project structure should match the `step-1-initialize-with-ph-init` branch. You should have: - Empty `document-models/`, `editors/`, `processors/`, and `subgraphs/` directories - Configuration files: `powerhouse.config.json`, `powerhouse.manifest.json` - Package management files: `package.json`, `pnpm-lock.yaml` - Build configuration: `tsconfig.json`, `vite.config.ts`, `vitest.config.ts` ### Compare with reference implementation Verify your initial setup matches the tutorial: ```bash # Compare your project structure with step-1 git diff tutorial/step-1-initialize-with-ph-init # List files in the tutorial's step-1 git ls-tree -r --name-only tutorial/step-1-initialize-with-ph-init # View a specific config file from step-1 git show tutorial/step-1-initialize-with-ph-init:package.json ```` ## ph install / ph add ```bash ph install [dependencies...] [--registry ] [--local] [--allow-build ] ``` Aliases: `ph add`, `ph i` The install command adds Powerhouse dependencies to your project. By default it only registers the package in `powerhouse.config.json` with provider `"registry"` โ€” Connect will load it from the registry CDN at runtime. With `--local`, the package is also installed into `node_modules` and marked as provider `"local"` โ€” it will be bundled into `ph connect build` so the preview works without the registry being reachable. Resolution order for the registry URL: `--registry` flag > `PH_REGISTRY_URL` env > `powerhouse.config.json` > default ## Up next In the next tutorials, you will learn how to specify, add code and build an editor for your document model and export it to be used in your Powerhouse package. --- ## Write the document specification > Source: https://powerhouse.academy/academy/GetStarted/DefineToDoListDocumentModel **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-2-generate-todo-list-document-model](https://github.com/powerhouse-inc/todo-tutorial/tree/step-2-generate-todo-list-document-model) This tutorial step has a corresponding branch. After completing this step, your project will have a generated document model with: - Document model specification files (`todo-list.json`, `schema.graphql`) - Auto-generated TypeScript types and action creators - Reducer scaffolding ready for implementation :::
๐Ÿ“– How to use this tutorial **Prerequisites**: Complete step 1 and set up the tutorial remote (see previous step). ### Compare your generated code After running `ph generate TodoList.phdm.zip`, compare with the reference: ```bash # Compare all generated files with step-2 git diff tutorial/step-2-generate-todo-list-document-model # Compare specific directory git diff tutorial/step-2-generate-todo-list-document-model -- document-models/todo-list/ ``` ### See what was generated View the complete step-2 reference code: ```bash # List files in the tutorial's step-2 git ls-tree -r --name-only tutorial/step-2-generate-todo-list-document-model document-models/ # View a specific file from step-2 git show tutorial/step-2-generate-todo-list-document-model:document-models/todo-list/schema.graphql ``` ### Visual comparison with GitHub Desktop After making a commit, use GitHub Desktop for visual diff: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-2-generate-todo-list-document-model` 3. Review all file differences in the visual interface See step 1 for detailed GitHub Desktop instructions.
In this tutorial, you will learn how to define the specifications for a **todo-list** document model within Vetra Studio using its GraphQL schema, and then export the resulting document model specification document for your Powerhouse project. If you don't have a document specification file created yet, have a look at the previous step of this tutorial to create a new document specification. ## TodoList document specification We'll continue with this project to teach you how to create a document model specification and later an editor for your document model. We use the **GraphQL Schema Definition Language** (SDL) to define the schema for the document model. Below, you can see the SDL for the `TodoList` document model. **INFO:** This schema defines the **data structure** of the document model and the types involved in its operations, which are detailed further as input types. Documents in Powerhouse leverage **event sourcing principles**, where every state transition is represented by an operation. GraphQL input types describe operations, ensuring that user intents are captured effectively. These operations detail the parameters needed for state transitions. The use of GraphQL aligns these transitions with explicit, validated, and reproducible commands. This is the essence of **Specification Driven Design & Development**: your schema serves as a machine-readable specification that both humans and AI agents can understand and executeโ€”turning your intent into precise, maintainable functionality.
State schema of our simplified TodoList ```graphql # The state of our TodoList - contains an array of todo items type TodoListState { items: [TodoItem!]! } # A single to-do item with its properties type TodoItem { id: OID! # Unique identifier for each to-do item text: String! # The text description of the to-do item checked: Boolean! # Status of the to-do item (checked/unchecked) } ```
Operations schema of our simplified TodoList ```graphql # Defines a GraphQL input type for adding a new to-do item # Only text is required - ID is generated automatically, checked defaults to false input AddTodoItemInput { text: String! # The text for the new todo item } # Defines a GraphQL input type for updating a to-do item # ID is required to identify which item to update # text and checked are optional - only provided fields will be updated input UpdateTodoItemInput { id: OID! # Required: which item to update text: String # Optional: new text value checked: Boolean # Optional: new checked state } # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! # The ID of the item to delete } ```
## Define the document model specification ### The steps below show you how to do this: 1. In Vetra Studio, click on **'document model'** to open the document model specification editor. 2. Name your document model `TodoList` (PascalCase, no spaces or hyphens) in the Connect application. **Pay close attention to capitalization, as it influences code generation.** 3. You'll be presented with a form to fill in metadata about the document model. Fill in the details in the respective fields. In the **Document Type** field, type `powerhouse/todo-list` (lowercase with hyphen). This defines the new type of document that will be created with this document model specification. ![TodoList Document Model Form Metadata](../docs/docs/images/DocumentModelHeader.png) 4. In the code editor, you can see the SDL for the document model. Replace the existing SDL template with the SDL defined in the [State Schema](#state-schema) section. Only copy and paste the types, leaving the inputs for the next step. You can, however, already press the 'Sync with schema' button to set the initial state of your document model specification based on your Schema Definition Language. 5. Below the editor, find the input field `Add module`. You'll use this to create and name a module for organizing your input operations. In this case, we will name the module `todos`. Press enter. 6. Now there is a new field, called `Add operation`. Here you will have to add each input operation to the module, one by one. 7. Inside the `Add operation` field, type `ADD_TODO_ITEM` and press enter. A small editor will appear underneath it, with an empty input type that you have to fill. Copy the first input type from the [Operations Schema](#operations-schema) section and paste it in the editor. The editor should look like this: ```graphql input AddTodoItemInput { text: String! } ``` 8. Repeat the process from step 7 for the other input operations: `UPDATE_TODO_ITEM` and `DELETE_TODO_ITEM`. You may have noticed that you only need to add the name of the operation (e.g., `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`) without the `Input` suffix. It will then be generated once you press enter. 9. In the meantime Vetra has been keeping an eye on your inputs and started code generation in your directory. In your terminal you will also find any validation errors that help you to identify missing specifications. Check below screenshot for the complete implementation: ![ToDoList Document Model](../docs/docs/images/DocumentModelOperations.png) ## Verify your document model generation If you have been watching the terminal in your IDE you will see that Vetra has been tracking your changes and scaffolding your directory. It will mention: ``` โ„น [Vetra] Document model TodoList is valid, proceeding with code generation ``` Your project should have the following structure in `document-models/todo-list/`: ``` document-models/todo-list/ โ”œโ”€โ”€ gen/ # Auto-generated code (don't edit) โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”œโ”€โ”€ creators.ts # Action creator functions โ”‚ โ”œโ”€โ”€ types.ts # TypeScript type definitions โ”‚ โ”œโ”€โ”€ reducer.ts โ”‚ โ””โ”€โ”€ todos/ โ”‚ โ””โ”€โ”€ operations.ts # Operation type definitions โ”œโ”€โ”€ src/ # Your custom implementation โ”‚ โ”œโ”€โ”€ reducers/ โ”‚ โ”‚ โ””โ”€โ”€ todos.ts # Reducer functions (to implement next) โ”‚ โ””โ”€โ”€ tests/ โ”‚ โ””โ”€โ”€ todos.test.ts # Test file scaffolding โ”œโ”€โ”€ todo-list.json # Document model specification โ””โ”€โ”€ schema.graphql # GraphQL schema ``` **TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Compare your generated files with step-2 git diff tutorial/step-2-generate-todo-list-document-model -- document-models/todo-list/ ``` ### Up next: reducers Up next, you'll learn how to implement the runtime logic and components that will use the `TodoList` document model specification you've just created and exported. --- ## Implement the document model reducers > Source: https://powerhouse.academy/academy/GetStarted/ImplementOperationReducers **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-3-implement-reducer-operation-handlers](https://github.com/powerhouse-inc/todo-tutorial/tree/step-3-implement-reducer-operation-handlers) This step focuses on implementing the reducer logic for add, update, and delete operations.
๐Ÿ“– How to use this tutorial ### Compare your reducer implementation After implementing your reducers: ```bash # Compare your reducers with the reference git diff tutorial/step-3-implement-reducer-operation-handlers -- document-models/todo-list/src/reducers/ # View the reference reducer implementation git show tutorial/step-3-implement-reducer-operation-handlers:document-models/todo-list/src/reducers/todos.ts ``` ### Visual comparison with GitHub Desktop After committing your work, compare visually: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-3-implement-reducer-operation-handlers` 3. Review differences in the visual interface ### If you get stuck View or reset to a specific step: ```bash # View the reducer code git show tutorial/step-3-implement-reducer-operation-handlers:document-models/todo-list/src/reducers/todos.ts # Reset to this step (WARNING: loses your changes) git reset --hard tutorial/step-3-implement-reducer-operation-handlers ```
In this section, we will implement the operation reducers for the **todo-list** document model. In the previous step Vetra imported our document specification and scaffolded our code and directory through live code generation. **INFO:** Reducers are a core concept in Powerhouse document models. They implement the state transition logic for each operation defined in your schema. **Connection to schema definition language (SDL)**: The reducers directly implement the operations you defined in your SDL. Remember how we defined `AddTodoItemInput`, `UpdateTodoItemInput`, and `DeleteTodoItemInput` in our schema? The reducers provide the actual implementation of what happens when those operations are performed. ## Explore the generated reducer file Navigate to `/document-models/todo-list/src/reducers/todos.ts` and open it. You should see scaffolding code that needs to be filled for the three operations you specified: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` ## Implement the operation reducers Let's implement each reducer one by one. ### Step 1: Add the import First, add the `generateId` import at the top of the file: ```typescript // added-line ``` ### Step 2: Implement addTodoItemOperation Replace the boilerplate `addTodoItemOperation` with the actual implementation: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { // removed-start addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, // removed-end // added-start addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, // added-end updateTodoItemOperation(state, action) { // ... }, deleteTodoItemOperation(state, action) { // ... }, }; ``` **What's happening here:** - We generate a unique ID using `generateId()` from `document-model/core` - We push a new item to the `items` array with the input text, new ID, and `checked: false` - Under the hood, Powerhouse uses Immer.js, so this "mutation" is actually immutable ### Step 3: Implement updateTodoItemOperation Replace the boilerplate `updateTodoItemOperation`: ```typescript // removed-start updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, // removed-end // added-start updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // added-end ``` **What's happening here:** - We find the item by its ID - We return early if the item is not found - We use nullish coalescing (`??`) to only update fields that are provided ### Step 4: Implement deleteTodoItemOperation Replace the boilerplate `deleteTodoItemOperation`: ```typescript // removed-start deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, // removed-end // added-start deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, // added-end ``` **What's happening here:** - We filter out the item with the matching ID - This creates a new array without the deleted item ## Complete reducer file Here's the complete implementation:
Complete todos.ts ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ```
**TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Compare with reference implementation git diff tutorial/step-3-implement-reducer-operation-handlers -- document-models/todo-list/src/reducers/ ``` ## Up next: Writing tests In the next chapter, you'll write comprehensive tests to verify your reducer implementations work correctly. --- ## Write document model tests > Source: https://powerhouse.academy/academy/GetStarted/WriteDocumentModelTests **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-4-implement-tests-for-todos-operations](https://github.com/powerhouse-inc/todo-tutorial/tree/step-4-implement-tests-for-todos-operations) This step focuses on writing comprehensive tests for the reducers you implemented in the previous step.
๐Ÿ“– How to use this tutorial ### Compare your tests After writing tests: ```bash # Compare your tests with the reference git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/ # View the reference test implementation git show tutorial/step-4-implement-tests-for-todos-operations:document-models/todo-list/src/tests/todos.test.ts ``` ### Visual comparison with GitHub Desktop After committing your work, compare visually: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-4-implement-tests-for-todos-operations` 3. Review differences in the visual interface
In order to make sure the operation reducers are working as expected, you need to write tests for them. When you generated your document model code, we created some boilerplate tests for you. Now we'll enhance them to properly verify our reducer logic. ## Understanding the generated test file Navigate to `/document-models/todo-list/src/tests/todos.test.ts`. You will see that we have some basic "sanity check" style tests already. These make sure that your operations at least result in a valid document model state. ```typescript reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { // The `createDocument` utility function from your document model creates // a new empty document, i.e. one with your default initial state const document = utils.createDocument(); // The generateMock function takes one of your generated input schemas // and creates an object populated with random values for each field const input = generateMock(AddTodoItemInputSchema()); // We call your document model's reducer with the new document we just created // and the action we want to test, `addTodoItem` in this case. // The reducer returns a new object, which is the document with the action applied. // If successful, there will be an operation which corresponds to this action // in the updated document's operations list. const updatedDocument = reducer(document, addTodoItem(input)); // When you generate a document model, we give you validation utilities like // `isTodoListDocument` which confirms the document is of the correct form // in a way that TypeScript recognizes expect(isTodoListDocument(updatedDocument)).toBe(true); // At the start a document will have 0 operations, so after applying this action // there should now be one operation expect(updatedDocument.operations.global).toHaveLength(1); // The operation added to the list should correspond to the correct action type expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); }); it("should handle updateTodoItem operation", () => { // ... boilerplate test }); it("should handle deleteTodoItem operation", () => { // ... boilerplate test }); }); ``` ## Enhance the tests The boilerplate tests check that operations are applied, but they don't verify the **actual results**. Let's write more comprehensive tests. ### Test 1: Update the addTodoItem test The add test is already fairly complete. We just need to add type annotations: ```typescript it("should handle addTodoItem operation", () => { const document = utils.createDocument(); // added-line const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); ``` ### Test 2: Replace the updateTodoItem test Delete the existing boilerplate and add two separate tests - one for updating text, one for updating the checked state: ```typescript // removed-start it("should handle updateTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(UpdateTodoItemInputSchema()); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // ... }); // removed-end ``` **Add the new test for updating text:** ```typescript it("should handle updateTodoItem operation to update text", () => { // We need there to already be a todo item in the document, // since we want to test updating an existing item const mockItem = generateMock(TodoItemSchema()); // We also need to generate a mock input for the update operation we are testing const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // Since the mocks are generated with random values, we need to set the `id` // on our mock input to match the `id` of the existing mock item input.id = mockItem.id; // We want to easily check if the item's text was updated to our new value, // so we assign a variable and use that for the mock input's text field const newText = "new text"; input.text = newText; // We are only testing updating the text here, so we want the checked field // on the input to be undefined, i.e. it should not change anything on the existing item input.checked = undefined; // We can pass a different initial state to the `createDocument` utility, // so in this case we pass in an `items` array with our existing item already in it const document = utils.createDocument({ global: { items: [mockItem], }, }); // Create an updated document by applying the reducer with the action and input const updatedDocument = reducer(document, updateTodoItem(input)); // Use our validator to check that the document conforms to the document model schema expect(isTodoListDocument(updatedDocument)).toBe(true); // There should now be one operation in the operations list expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // Find the updated item in the items list by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // The item's text should now be updated to be our new text expect(updatedItem?.text).toBe(newText); // The item's `checked` field should be unchanged expect(updatedItem?.checked).toBe(mockItem.checked); }); ``` **Add the new test for updating checked state:** ```typescript it("should handle updateTodoItem operation to update checked", () => { // Generate a mock existing item const mockItem = generateMock(TodoItemSchema()); // Generate a mock input const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // Set the mock input's `id` to the mock item's `id` input.id = mockItem.id; // We want the new `checked` field value to be the opposite of the // randomly generated value from the mock const newChecked = !mockItem.checked; input.checked = newChecked; // Leave the `text` field unchanged input.text = undefined; // Create a document with the existing item in it const document = utils.createDocument({ global: { items: [mockItem], }, }); // Apply the reducer with the action and the mock input const updatedDocument = reducer(document, updateTodoItem(input)); // Validate your document expect(isTodoListDocument(updatedDocument)).toBe(true); // Get the updated item by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // The item's `text` field should remain unchanged expect(updatedItem?.text).toBe(mockItem.text); // The item's `checked` field should be updated to our new checked value expect(updatedItem?.checked).toBe(newChecked); }); ``` ### Test 3: Update the deleteTodoItem test The boilerplate delete test passes even without an existing item to delete. Let's fix that: ```typescript it("should handle deleteTodoItem operation", () => { // removed-start const document = utils.createDocument(); const input = generateMock(DeleteTodoItemInputSchema()); // removed-end // added-start // Create an existing item to delete const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock(DeleteTodoItemInputSchema()); input.id = mockItem.id; // added-end const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); // added-start // Verify the item was actually removed const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); // added-end }); ``` ## Complete test file Here's the complete test file with all updates. Don't forget to add the missing imports:
Complete todos.test.ts ```typescript AddTodoItemInput, DeleteTodoItemInput, UpdateTodoItemInput, } from "todo-tutorial/document-models/todo-list"; reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, TodoItemSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newText = "new text"; input.text = newText; input.checked = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(newText); expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newChecked = !mockItem.checked; input.checked = newChecked; input.text = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(mockItem.text); expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock( DeleteTodoItemInputSchema(), ); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ```
**TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Run tests pnpm test # Compare with reference implementation git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/ ``` Expected test output: ```bash โœ“ document-models/todo-list/src/tests/document-model.test.ts (3 tests) 1ms โœ“ document-models/todo-list/src/tests/todos.test.ts (4 tests) 8ms Test Files 2 passed (2) Tests 7 passed (7) ``` ## Up next: Building the editor In the next chapter, you'll learn how to implement a user interface (editor) for your document model so you can interact with it visually. --- ## Build a to-do list editor > Source: https://powerhouse.academy/academy/GetStarted/BuildToDoListEditor **TIP:** ๐Ÿ“ฆ **Reference Code**: - **Editor Scaffolding**: [step-5-generate-todo-list-document-editor](https://github.com/powerhouse-inc/todo-tutorial/tree/step-5-generate-todo-list-document-editor) - **Complete Editor UI**: [step-6-add-basic-todo-editor-ui-components](https://github.com/powerhouse-inc/todo-tutorial/tree/step-6-add-basic-todo-editor-ui-components) This tutorial covers two steps: 1. **Step 5**: Generating the editor template with Vetra Studio 2. **Step 6**: Building a complete, interactive UI with components for adding, editing, and deleting todos Compare implementations: `git diff step-5-generate-todo-list-document-editor step-6-add-basic-todo-editor-ui-components`
๐Ÿ“– How to use this tutorial This tutorial shows building from **generated scaffolding** (step-5) to **complete UI** (step-6). ### Compare your generated editor After running `ph generate --editor`: ```bash # Compare generated scaffolding with step-5 git diff tutorial/step-5-generate-todo-list-document-editor -- editors/ # View the generated editor template git show tutorial/step-5-generate-todo-list-document-editor:editors/todo-list-editor/editor.tsx ``` ### Compare your custom components After building your UI: ```bash # Compare your complete editor with step-6 git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/ # See what was added from scaffolding to complete UI git diff tutorial/step-5-generate-todo-list-document-editor..tutorial/step-6-add-basic-todo-editor-ui-components ``` ### Browse the complete implementation Explore the production-ready component structure: ```bash # List all components in step-6 git ls-tree -r --name-only tutorial/step-6-add-basic-todo-editor-ui-components editors/todo-list-editor/components/ # View a specific component git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/TodoList.tsx ``` ### Visual comparison with GitHub Desktop After committing your editor code: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-5-generate-todo-list-document-editor` or `tutorial/step-6-add-basic-todo-editor-ui-components` 3. See all your custom components vs. the reference implementation See step 1 for detailed GitHub Desktop instructions.
In this chapter we will continue with the interface or editor implementation of the **todo-list** document model. This means you will create a simple user interface for the **todo-list** document model which will be used inside Connect to create, update and delete your todo-list items. ## Add a document editor specification in Vetra Studio. Go back to Vetra Studio and click the 'Add new specification' button in the User Experiences column under 'Editors'. This will create an editor template for your document model. Give the editor the name `todo-list-editor` and select the correct document model. In our case that's the `powerhouse/todo-list` ### Editor implementation options When building your editor component within the Powerhouse ecosystem, you have several options for styling, allowing you to leverage your preferred methods: 1. **Default HTML Styling:** Standard HTML tags (`

`, `

`, ` ); } ``` **What's happening here:** - We use a form with `onSubmit` handler for better UX (Enter key support) - We extract the text value from the input field - We dispatch the `addTodoItem` action (generated from our SDL) - We reset the form after submission ### Step 4: Create the Todos list component Create `editors/todo-list-editor/components/Todos.tsx` to render the list of todos: ```tsx type Props = { todos: TodoItem[]; }; /** Shows a list of the todo items in the selected todo list */ export function Todos({ todos }: Props) { const hasTodos = todos.length > 0; if (!hasTodos) { return

Start adding things to your todo list

; } return (
    {todos.map((todo) => (
  • ))}
); } ``` **What's happening here:** - We accept `todos` as a prop (passed from `TodoList` parent) - We show a helpful message if the list is empty - We map over todos and render a `Todo` component for each item ### Step 5: Create the Todo item component Create `editors/todo-list-editor/components/Todo.tsx` for individual todo items with edit and delete functionality: ```tsx useState, type ChangeEventHandler, type FormEventHandler, type MouseEventHandler, } from "react"; deleteTodoItem, updateTodoItem, } from "todo-tutorial/document-models/todo-list"; type Props = { todo: TodoItem; }; /** Displays a single todo item in the selected todo list * * Allows checking/unchecking the todo item. * Allows editing the todo item text. * Allows deleting the todo item. */ export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); // Even though this component is for a single todo item and not the whole list, // we can use the exact same hook for dispatching updates to it. // The dispatch function works for any action supported by a TodoList document. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const todoId = todo.id; const todoText = todo.text; const todoChecked = todo.checked; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; // We can use the dispatch function for any of the actions // supported by a TodoList document dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch( updateTodoItem({ id: todo.id, checked: event.target.checked, }), ); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todoId })); }; const onClickEditTodo: MouseEventHandler = () => { setIsEditing(true); }; const onClickCancelEditTodo: MouseEventHandler = () => { setIsEditing(false); }; if (isEditing) return (
); return (
{todoText}
); } ``` **What's happening here:** - We use local state (`isEditing`) to toggle between view and edit modes - We dispatch `updateTodoItem` for both checking and text editing - We dispatch `deleteTodoItem` to remove items - We use TypeScript event handlers for type safety ### Step 6: Create the TodoListName component Finally, create `editors/todo-list-editor/components/TodoListName.tsx` for displaying and editing the document name: ```tsx /** Allows editing the name of the selected todo list */ export function TodoListName() { const [isEditing, setIsEditing] = useState(false); const [selectedTodoList, dispatch] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const documentName = selectedTodoList.name; const onSubmitEditName: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const nameInput = form.elements.namedItem("name") as HTMLInputElement; const name = nameInput.value; if (name) { dispatch(setName(name)); setIsEditing(false); } }; if (isEditing) { return (
); } return (

setIsEditing(true)} > {documentName}

); } ``` **What's happening here:** - We use the `setName` action from `document-model/document` (a built-in action) - We toggle between viewing and editing the name - Click the name to edit it ## Test your editor Now you can run the Vetra Studio Preview and see the **todo-list** editor in action: ```bash ph vetra --watch ``` In the bottom right corner you'll find a new Document Model that you can create: **todo-list**. Click on it to create a new todo-list document. **INFO:** The editor will update dynamically as you make changes, so you can experiment with styling and functionality while seeing your results appear in Vetra Studio in real-time. **Try it out:** 1. Add some todo items using the input form 2. Click on the document name to edit it 3. Check/uncheck items to mark them as complete 4. Click "Edit" on any item to modify its text 5. Click "Delete" to remove items Congratulations! ๐ŸŽ‰ If you managed to follow this tutorial until this point, you have successfully implemented the **todo-list** document model with its reducer operations and editor. ## Compare with the reference implementation The tutorial repository's step-6 branch includes additional enhancements you can explore: **Additional components in step-6:** ``` editors/todo-list-editor/components/ โ”œโ”€โ”€ CloseButton.tsx # Editor close functionality โ”œโ”€โ”€ UndoRedoButtons.tsx # Operation history navigation โ””โ”€โ”€ Stats.tsx # Display metadata (creation/modification times) ``` **View individual components from the reference:** ```bash # See the enhanced TodoList component with all features git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/TodoList.tsx # Explore the UndoRedoButtons component git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/UndoRedoButtons.tsx # Compare your implementation with the reference git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/todo-list-editor/ ``` **TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Run tests pnpm test # Test in Vetra Studio ph vetra --watch # Compare with reference implementation git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/todo-list-editor/ ``` In Connect, you should be able to: - Create a new todo-list document - Add, edit, and delete todo items - Check/uncheck items to mark them complete ## Key concepts learned In this tutorial you've learned: โœ… **Component-based architecture** - Breaking down complex UIs into reusable components โœ… **Document model hooks** - Using `useSelectedTodoListDocument` to connect React to your document state โœ… **Action dispatching** - How to dispatch operations (`addTodoItem`, `updateTodoItem`, `deleteTodoItem`) from your UI โœ… **Type-safe development** - Leveraging TypeScript with generated types from your SDL โœ… **Form handling** - Using React forms with proper event handlers โœ… **Local vs. document state** - When to use React `useState` vs. document model state ### Up next: Mastery Track In the [Mastery Track chapter: Document Model Creation](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) we guide you through the theoretics of the previous steps while creating a more advanced version of the todo-list. You will learn: - The ins and outs of a document model. - How to use UI & Scalar components from the Document Engineering system. - How to build Custom drive-apps or Drive Explorers. --- # Mastery Track ## Prerequisites > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/Prerequisites Let's set up your machine to start building your first Document Model. Don't worry if this is your first time setting up a development environment - we'll guide you through each step! **INFO:** If you've already set up **Git, Node.js 24, and a package manager (pnpm or npm)**, your most important step is to install the **Powerhouse CLI** with the command: `pnpm install -g ph-cmd` or `npm install -g ph-cmd`. A global install is recommended if you want to use the command from any directory as a power user. The Powerhouse CLI is used to create, build, and run your Document Models and gives you direct access to a series of Powerhouse Builder Tools. Move to the end of this page to [verify your installation.](#verify-installation) --- ## Overview Before we begin building our Document Model, we need to install some software on your machine. We'll need three main tools: - Node.js 24, which helps us run our code. - Visual Studio Code (VS Code), which is where we'll write our code - Git, which helps us manage our code. Follow the steps below based on your computer's operating system. ### Windows Users: Consider Using WSL If you're on Windows, we recommend using **Windows Subsystem for Linux (WSL)** for the best development experience. WSL lets you run a full Linux environment directly on Windows without the overhead of a virtual machine. This gives you access to Linux command-line tools and utilities, which are often preferred for modern web development workflows. **To install WSL:** 1. Open PowerShell as Administrator (right-click and select "Run as administrator") 2. Run the following command: ```powershell wsl --install ``` 3. Restart your computer when prompted 4. After restart, a terminal will open asking you to create a Linux username and password Once WSL is set up, you can follow the **Linux (Ubuntu/Debian)** instructions below for installing Node.js, Git, and other tools. Your Linux environment will be accessible through the Windows Terminal or by typing `wsl` in PowerShell. **TIP:** Using WSL provides a consistent development experience that matches most production environments and online tutorials. You can still use VS Code on Windows โ€” it integrates seamlessly with WSL through the [Remote - WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl). For more details, see the official [WSL installation guide](https://learn.microsoft.com/en-us/windows/wsl/install). --- ### Installing Node.js 24 Node.js 24 is a tool that lets us run our application. Let's install it step by step. #### For Windows: 1. **Set up PowerShell for running commands:** - Press the Windows key - Type "PowerShell" - Right-click on "Windows PowerShell" and select "Run as administrator" - In the PowerShell window, type this command and press Enter: ```powershell Set-ExecutionPolicy RemoteSigned -Scope CurrentUser ``` - Type 'A' when prompted to confirm - You can now close this window and open PowerShell normally for the remaining steps 2. **Install Node.js 24:** - Visit the [Node.js official website](https://nodejs.org/) - Click the big green button that says "LTS" (this means Long Term Support - it's the most stable version) - Once the installer downloads, double-click it to start installation - Click "Next" through the installation wizard, leaving all settings at their defaults 3. **Install a package manager (pnpm or npm):** - Open PowerShell (no need for admin mode) - For pnpm (recommended), type this command and press Enter: ```powershell npm install -g pnpm ``` - Note: Node.js comes with npm by default, so npm is already available after installing Node.js 4. **Verify Installation:** - Open PowerShell (no need for admin mode) - Type these commands one at a time and press Enter after each: ```powershell node --version pnpm --version # or npm --version ``` - You should see version numbers appear after each command (e.g., v24.x.x for Node.js). If you do, congratulations - Node.js and your package manager are installed! > **Note**: If Node.js commands don't work in VS Code, restart VS Code to refresh environment variables. #### For macOS: 1. **Install Homebrew:** - Open Terminal (press Command + Space and type "Terminal") - Copy and paste this command into Terminal and press Enter: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` - Follow any additional instructions that appear 2. **Install Node.js 24:** - In the same Terminal window, type this command and press Enter: ```bash brew install node@24 ``` - Then, optionally install pnpm (npm comes with Node.js): ```bash brew install pnpm ``` 3. **Verify Installation:** - In Terminal, type these commands one at a time and press Enter after each: ```bash node --version pnpm --version # or npm --version ``` - If you see version numbers, you've successfully installed Node.js and your package manager! #### For Linux (Ubuntu/Debian): 1. **Open Terminal:** - Press Ctrl + Alt + T on your keyboard, or - Click the Activities button and type "Terminal" 2. **Update Package List:** ```bash sudo apt update ``` 3. **Install Node.js 24 and optionally pnpm:** ```bash sudo apt install nodejs # Optionally install pnpm (npm comes with Node.js) sudo apt install pnpm ``` 4. **Verify Installation:** - Type these commands one at a time and press Enter after each: ```bash node --version pnpm --version # or npm --version ``` - If you see version numbers, you're all set! ### Installing Visual Studio Code VS Code is the editor we'll use to write our code. Here's how to install it: #### For Windows: 1. Visit the [Visual Studio Code website](https://code.visualstudio.com/) 2. Click the blue "Download for Windows" button 3. Once the installer downloads, double-click it 4. Accept the license agreement and click "Next" 5. Leave the default installation location and click "Next" 6. In the Select Additional Tasks window, make sure "Add to PATH" is checked 7. Click "Next" and then "Install" 8. When installation is complete, click "Finish" #### For macOS: 1. Visit the [Visual Studio Code website](https://code.visualstudio.com/) 2. Click the blue "Download for Mac" button 3. Once the .zip file downloads, double-click it to extract 4. Drag Visual Studio Code.app to the Applications folder 5. Double-click the app to launch it 6. To make VS Code available in your terminal: - Open VS Code - Press Command + Shift + P - Type "shell command" and select "Shell Command: Install 'code' command in PATH" #### For Linux (Ubuntu/Debian): 1. Open Terminal (Ctrl + Alt + T) 2. First, update the packages list: ```bash sudo apt update ``` 3. Install the dependencies needed to add Microsoft's repository: ```bash sudo apt install software-properties-common apt-transport-https wget ``` 4. Import Microsoft's GPG key: ```bash wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add - ``` 5. Add the VS Code repository: ```bash sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" ``` 6. Install VS Code: ```bash sudo apt install code ``` 7. Once installed, you can launch VS Code by: - Typing `code` in the terminal, or - Finding it in your Applications menu ### Install Git #### For Windows 1. Open PowerShell (press Windows key, type "PowerShell", and press Enter) 2. Visit the [Git website](https://git-scm.com/) 3. Download the latest version for Windows 4. Run the installer and use the recommended settings 5. Verify installation by opening PowerShell: ```powershell git --version ``` #### For macOS 1. Install using Homebrew: ```bash brew install git ``` 2. Verify installation: ```bash git --version ``` #### For Linux (Ubuntu/Debian) 1. Update package list: ```bash sudo apt update ``` 2. Install Git: ```bash sudo apt install git ``` 3. Verify installation: ```bash git --version ``` ### Configure Git (All Systems) After installation, set up your identity: ```bash git config --global user.name "Your Name" git config --global user.email "your.email@example.com" ``` ### Install the Powerhouse CLI The Powerhouse CLI (installed via the `ph-cmd` package) is a command-line interface tool. It provides the `ph` command, which is essential for managing Powerhouse projects. You can get access to the Powerhouse Ecosystem tools by installing them globally using: ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` Key commands include: - `ph connect` for running the Connect application locally - `ph switchboard` or `ph reactor` for starting the API service - `ph init` to start a new project and build a document model - `ph help` to get an overview of all the available commands This tool will be fundamental on your journey when creating, building, and running Document Models.
How to use different branches? When installing or using the Powerhouse CLI commands you can use the dev & staging branches. These branches contain more experimental features than the latest stable release the PH CLI uses by default. They can be used to get access to a bug fix or features under development. | Command | Description | | ---------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** | Install latest stable version | | **pnpm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
Managing ph-cmd Versions and Package Information ### Switching Between Versions To change to a different version of `ph-cmd`, reinstall it globally with your package manager: ```bash # Install latest version with a specific tag npm install -g ph-cmd@staging pnpm install -g ph-cmd@dev # Install a specific version number npm install -g ph-cmd@1.2.3-staging.10 pnpm install -g ph-cmd@6.0.0-dev.33 ``` **Important:** Always use the same package manager you used for the original global install to avoid conflicting installations. ### Checking Your Installation Use the `which` command to see where your global install is located: ```bash which ph # Example output: /Users/username/Library/pnpm/ph ``` This shows which package manager was used (in this case, pnpm). ### Viewing Available Versions Use `npm view` to see all available versions and tags: ```bash npm view ph-cmd ``` **Example dist-tags output:** ``` dist-tags: latest: 5.3.0 dev: 6.0.0-dev.33 staging: 5.3.0-staging.24 test: 2.5.0-test.0 ``` ### Best Practices - Use specific version numbers instead of tags when you need exact version consistency - Check `npm view ph-cmd` before switching to see the latest available versions - Remember that without specifying a version, `@latest` is installed by default
### Verify Installation Open your terminal (command prompt) and run the following commands to verify your setup: ```bash node --version pnpm --version git --version ph --version ``` You should see version numbers displayed for all commands, similar to the example output below (your versions might be higher). The output for `ph --version` includes its version and may also show additional messages if further setup like `ph setup-globals` is needed. You're now ready to start building your first Document Model! ```bash % node --version v24.13.0 % pnpm --version 10.10.0 % git --version git version 2.39.3 % ph --version PH CMD version: 0.43.18 ------------------------------------- PH CLI is not available, please run `ph setup-globals` to generate the default global project ``` --- ## Vetra Studio > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/VetraStudio ## Introducing Vetra Studio Vetra Studio is the builder environment where you create, manage, and collaborate on Powerhouse packages. It consists of two main components: - **Vetra Studio Drive**: Serves as a hub for developers to access, manage & share specifications through a remote Vetra drive. It functions as the orchestration hub where you as a builder assemble all the necessary specifications for your intended use-case, software solution, or package. Each specification document corresponds to a **module** โ€” a distinct building block of your package (such as a document model, editor, or data integration). - **Vetra Package Library**: Store, publish, and fork git repositories of packages in the Vetra Package Library. Visit the [Vetra Package Library here](https://vetra.io/packages) **INFO:** What is a Specification Document? A **specification document** is a configuration file that defines how a specific module in your package should behave. Think of it as a blueprint โ€” it describes the structure, rules, and relationships that Powerhouse uses to generate the actual code for that module. These specification documents unlock **Specification Driven Design & Development**โ€”enabling you to communicate your solution and intent through a structured framework designed for AI collaboration. Specs serve as a shared language that enables precise, iterative editsโ€”turning messy intent into clean execution, and turning business needs into maintainable functionality. As Vetra Studio matures, each of these specification documents will offer an interface by which you as a builder get more control over the modules that make up your package. For now, the specification documents offer you a template for code generation.
Modules
The list of available modules color coded according to the 3 categories.
### Module Categories ### 1. Document Models A **document model** is a structured data type that defines what information your application can store and how it can be modified. Unlike traditional databases, document models use **operations** (actions like "add item" or "update title") rather than direct data manipulation, making them ideal for collaborative and auditable applications. - **Document model specification**: Defines the structure and operations of a document model using [GraphQL SDL](https://graphql.org/learn/schema/) (Schema Definition Language), ensuring consistent data management and processing. โ†’ [Learn more about Document Models](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) ### 2. User Experiences - **Editor specification**: Outlines the interface and functionalities of a document model editor, allowing users to interact with and modify document data. - **Drive-app specification**: Specifies the UI and interactions for managing documents within a drive, providing tailored views and functionalities. โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) โ†’ [Learn more about Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) ### 3. Data Integrations - **Subgraph specification**: Details the connections and relationships within a subgraph (a subset of your data exposed via a GraphQL API), facilitating efficient data querying and manipulation. - **Codegen Processor Specification**: Describes the process for automatically generating code from document model specifications, ensuring alignment with intended architecture. - **RelationalDb Processor Specification**: Defines how relational databases are structured and queried, supporting efficient data management and retrieval. โ†’ [Learn more about Using Subgraphs](/academy/MasteryTrack/WorkWithData/UsingSubgraphs) โ†’ [Learn more about Relational DB Processor](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor)
Vetra Studio Drive
The Vetra Studio Drive, a builder app that collects all of the specifications of a package.
### Configure a Vetra Drive in Your Project You can connect to a remote Vetra drive instead of using the local one auto-generated when you run `ph vetra` (where `ph` is short for "powerhouse", the CLI tool and the Organization behind Vetra). - **Without** the `--remote-drive` option: Vetra will create a local drive for you that lives in your browser's local storage. This is useful for solo development or experimentation. - **With** the `--remote-drive` argument: Vetra will connect to a remote drive instead of creating a local one. The remote drive can be hosted wherever you want (e.g., on your own server or a shared team environment). The Powerhouse config includes a Vetra URL for consistent project configuration across different environments. ```typescript vetra: { driveId: string; driveUrl: string; } ``` Imagine you are a builder and want to work on, or continue with a set of specifications from your teammates. You could then add the specific remote Vetra drive to your Powerhouse configuration in the `powerhouse.config.json` file to get going: ```json "vetra": { "driveId": "bai-specifications", "driveUrl": "https://switchboard.staging.vetra.io/d/bai-specifications" } ``` An example of a builder team building on the Powerhouse Vetra Ecosystem and its complementary Vetra Studio Drive specifications for the different packages can be found [here](https://vetra.io/builders/bai). ### Connect Claude to the Reactor MCP Claude can connect directly to your running Reactor via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), giving it live access to your drives, documents, and document model operations. Add the following configuration to your Claude MCP settings (e.g. `~/.claude/mcp.json` for Claude Code, or the MCP servers section in Claude Desktop): ```json { "mcpServers": { "reactor-mcp": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:4001/mcp"] } } } ``` This connects Claude to the Reactor running at `http://localhost:4001`. Make sure `ph vetra --watch` (or `ph reactor`) is running before starting a Claude session that uses the MCP. โ†’ See [Connecting Claude with Reactor MCP](/academy/Cookbook#connecting-claude-with-reactor-mcp) for a step-by-step walkthrough.
๐Ÿ“ฆ Vetra Remote Drive Commands Remote drives enable collaborative development by syncing specifications across team members. **Key Commands:** - `ph init --remote-drive ` - Create or connect to a project using a remote drive - `ph vetra --watch` - Start development with a preview drive for testing local changes **Workflows:** - **Project Owner**: `ph init --remote-drive` โ†’ Create GitHub repo โ†’ Push โ†’ `ph vetra --watch` to configure - **Collaborator**: `ph init --remote-drive ` โ†’ `ph vetra --watch` to start developing **Preview Drive (`--watch` mode):** The preview drive allows you to safely test changes before they affect the shared remote drive. - The main **"Vetra" drive** syncs with the remote and contains the stable package configuration. - The **"Vetra Preview" drive** is created locally for testing document models and editors before syncing your changes to the team. - When restarting Vetra, always use `ph vetra --watch` so it loads your local documents and editors. โ†’ [Full Vetra Remote Drive Reference](/academy/APIReferences/VetraRemoteDrive)
--- ## Create a package with Vetra > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/CreateAPackageWithVetra **INFO:** A **Powerhouse Package** is a distributable unit that bundles one or more document models, editors, and optional reactor modules into a single installable artifact. Once published to the Vetra registry, a package can be installed in any Vetra Cloud environment โ€” extending Connect with new document types and editors, and extending Switchboard with the corresponding reactor logic.
Create a Package
The five steps to build and publish a package to the Vetra ecosystem.
On Vetra Cloud you'll find these five steps as a quick reference for creating and publishing a package. This guide walks you through each step in detail โ€” from installing the CLI and scaffolding your project, to building document models and editors, and finally publishing your package to the Vetra registry so others can install it in their environments. **WARNING:** **This tutorial is a summary for builders that are already familiar with building document models**. It provides a summary from initial setup up to publishing a distributable package. Please start with the [**Get Started**](/) Chapter or [**Document Model Creation**](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema) section if you are unfamiliar with building a document model.
Key commands that you'll use in this flow - `pnpm install -g ph-cmd` or `npm install -g ph-cmd`: Installs the Powerhouse CLI globally. - `ph init`: Initializes a new Powerhouse project or sets up the local environment. - `ph vetra --interactive`: Launches Vetra Studio in interactive mode for package development. - `ph vetra --interactive --watch`: Launches Vetra Studio with dynamic reloading for document-models and editors. - `ph build`: Builds the project for production. - `pnpm run test` or `npm test`: Runs unit tests. - `ph publish`: Publishes your package to the Vetra registry. - `ph install @your-org-ph/your-package-name`: Installs a published package into a local Powerhouse environment.
## Phase 1: Setup and initialization ### 1.1. Install Powerhouse CLI Ensure you have the Powerhouse Command Line Interface (`ph-cmd`) installed. This tool is crucial for managing your Powerhouse projects. ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` **INFO:** See the [Prerequisites](/academy/MasteryTrack/BuilderEnvironment/Prerequisites) guide for detailed installation instructions for Node.js 24, package managers (pnpm or npm), and Git if you haven't set them up yet. ### 1.2. Initialize your project environment Before creating a specific project, or if you're setting up your environment for the first time, initialize the Powerhouse environment. This command prepares your local setup, including a local Reactor configuration. ```bash ph init my-package ``` Replace `my-package` with your desired package name. If you run `ph init` without a name you will be prompted for one.
How to make use of different branches? When installing or using the Powerhouse CLI commands you are able to make use of the dev & staging branches. These branches contain more experimental features than the latest stable release the PH CLI uses by default. They can be used to get access to a bugfix or features under development. | Command | Description | | ----------------------------------------------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** or **npm install -g ph-cmd** | Install latest stable version | | **pnpm install -g ph-cmd@dev** or **npm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** or **npm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
### 1.3. Launch Vetra Studio You can launch Vetra Studio in two modes: #### Interactive Mode (Recommended for Development) ```bash ph vetra --interactive ``` In interactive mode: - You'll receive confirmation prompts before any code generation - Changes require explicit confirmation before being processed - Provides better control and visibility over document changes #### Watch Mode ```bash ph vetra --interactive --watch ``` In watch mode: - Adding `--watch` to your command enables dynamic loading for document-models and editors in Vetra studio and switchboard. - When enabled, the system will watch for changes in these directories and reload them dynamically. **WARNING:** When you are building your document model the code can break the Vetra Studio environment. A full overview of the Vetra Studio commands can be found in the [Powerhouse CLI](/academy/APIReferences/PowerhouseCLI#vetra) #### Standard Mode ```bash ph vetra ``` In standard mode: - Changes are processed automatically with 1-second debounce - Multiple changes are batched and processed together - Uses the latest document state for processing
Alternatively: Use Connect Connect is your local development hub. Running it in Studio Mode spins up a local instance with a local Reactor, allowing you to define, build, and test document models. ```bash ph connect ``` This command typically opens Connect in your browser at `http://localhost:3000/`. **INFO:** **Powerhouse Reactors** are essential nodes in the Powerhouse network. They store documents, manage versions, resolve conflicts, and verify document operation histories by rerunning them. Reactors can be configured for local storage (as in Studio Mode), centralized cloud storage, or decentralized storage networks.
### 1.4. Launch Claude with Reactor-MCP Vetra Studio integrates deeply with Claude through MCP (Model Context Protocol). This is where AI comes into the mix and you get the chance to have greater control and direction over what your LLM is coding for you. **INFO:** Vetra embraces **Specification Driven Design & Development** โ€”an approach where your structured specification documents become the shared language between you and AI agents. You communicate intent through precise specs that are machine-readable and executable.
Explainer: Specification Driven AI In the **'Get Started'** chapter we've been making use of strict schema definition principles to communicate the intended use case of our document models. The **schema definition language** is not only a shared language that bridges the gap between developer, designer and analyst but also the gap between builder and AI-agent through **specification driven AI control**. - Communicate your solution and intent through a structured specification framework designed for AI collaboration. - Specifications enable precise, iterative edits, since all our specification documents are machine-readable and executable. #### Key Reactor MCP Features **Reactor-mcp** is a Model Context Protocol (MCP) server that bridges AI agents with Powerhouse document operations. - It supports automatic document model creation from natural language descriptions - It implements a smart editor based on the underlying document models - It automatically triggers code generation when documents reach valid state - The MCP server enables the agent to work with both existing and newly created document models - Vetra supports integration with custom remote drives, allowing users to create, share and manage documents within these drives **Document Operations:** - `createDocument` / `getDocument` / `deleteDocument` - Manage documents - `addActions` - Modify document state through operations **Drive Operations:** - `getDrives` / `addDrive` / `getDrive` - Manage document drives - `addRemoteDrive` - Connect to remote drives **Document Model Operations:** - `getDocumentModels` - List available document model types - `getDocumentModelSchema` - Get schema for specific models **Document Model Agent:** A specialized AI agent that guides users through document model creation with requirements gathering, design confirmation, and implementation including state schema definition, operation creation, and code generation.
#### 0. Configure the Reactor MCP Add the following to your Claude MCP settings (`~/.claude/mcp.json` for Claude Code, or the MCP servers section in Claude Desktop): ```json { "mcpServers": { "reactor-mcp": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:4001/mcp"] } } } ``` This only needs to be done once. Make sure `ph vetra` is running before starting a Claude session that uses the MCP. #### 1. Start the Reactor MCP: Make sure you are in the same directory as your project. Claude will automatically recognize the necessary files and MCP tools. ```bash claude ``` Since you're interacting with an LLM it has a high capacity for interpreting your intentions. Similar natural language commands will work as well. ```bash connect to the reactor mcp ``` #### 2. Verify MCP connection: - Check that the Reactor MCP is available. - Confirm Vetra Studio shows "Connected to Reactor MCP" ```bash Connected to MCP successfully! I can see there's a "vetra-4de7fa45" drive available. The reactor-mcp server is running and ready for document model operations. or Connected to reactor MCP. You have access to 1 drive: vetra+a049e1b1== ``` ## Phase 2: Package Creation ### 2.1. Set Package Description (Required) 1. Provide a name for your package 2. Add a meaningful description 3. Add keywords to add search terms to your package 4. Confirm changes when prompted in interactive mode ### 2.2. Define Document Model (Required) You can create document models in two ways: #### Manual Creation - Define document schema with fields and types as in the **'Get Started'** chapter - Create the necessary operations - Add the required modules to your package - The document model creation chapter in the Mastery track provides in depth support [here](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema) โ†’ [Learn more about Document Models](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) #### Using MCP (AI-Assisted) - Describe your package, it's functionality and your needs in natural language in great detail. - Claude will: - Generate an appropriate schema in the document model - Create the necessary operations - Implement the required reducers - Place the document in the Vetra drive - Claude will also add the necessary interface in the form of a [document editor](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) and scaffold the [drive-app functionality](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) when specified. โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) โ†’ [Learn more about Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer)
Alternatively: Use Connect Within Connect Studio Mode, navigate to the Document Model Editor. Here, you'll specify the structure of your document model using GraphQL Schema Definition Language (SDL). - **State Schema:** Describes the data fields and types within your document (e.g., `ToDoItem` with `id`, `text`, `checked` fields). - This schema is the blueprint for your document model's data. In the same editor, specify the operations (state transitions) for your document model. These are also defined using GraphQL, specifically input types. - **Operations Schema:** Specifies the actions that can be performed on the document (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`, `DeleteTodoItemInput`). - Each input type details the parameters required for an operation. - **Best Practices:** - Clearly define operations (often aligning with CRUD principles). - Use GraphQL input types for operation parameters. - Ensure operations reflect user intent for a clean API. Once your schema and operations are defined in Connect, export the specification. This will download a `.phdm.zip` file (e.g., `YourModelName.phdm.zip`). Save this file in the root of your Powerhouse project directory. Use the Powerhouse CLI to process an exported `.phdm.zip` file and generate the necessary boilerplate code for your document model. ```bash ph generate document-model --file YourModelName.phdm.zip ``` This command creates a new directory under `document-models/YourModelName/` containing: - A JSON file with the document model specification. - A GraphQL file with the state and operation schemas. - A `gen/` folder with autogenerated TypeScript types, action creators, and utility functions based on your schema. - A `src/` folder where you'll implement your custom logic (reducers, utils).
### 2.3. Add Document Editor (Required) #### Manual Creation - Select your target document model - Configure the currently limited editor properties - Add the editor specification to Vetra Studio drive - The system will generate scaffolding code #### Using MCP (AI-Assisted) - Request Claude to create an editor for your document. Do this with the help of a detailed description of the user interface, user experience and logic that you wish to generate. Make sure to reference operations from the document model to get the best results - Claude will: - Generate editor components - Implement necessary hooks - Create required UI elements โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors)
Alternatively: Generate command A document editor provides the user interface for interacting with your document model. Generate an editor template: ```bash ph generate editor --name YourModelName --document-type powerhouse/YourModelName ``` - The `--name YourModelName` flag specifies the document model this editor is for. - The `--document-type powerhouse/YourModelName` flag links the editor to the specific document type defined in your model specification. This creates a template file, typically at `editors/your-model-name/editor.tsx`. - Customize this React component to build your UI. - You can use standard HTML, Tailwind CSS (available in Connect), or import custom CSS. - Utilize components from `@powerhousedao/document-engineering` for consistency and rapid development. Learn more at [Document-Engineering](/academy/ComponentLibrary/DocumentEngineering)
## Phase 3: Implementation and testing ### 3.1. Implement reducer logic Reducers are pure functions that implement the state transition logic for each operation defined in your schema. Navigate to `document-models/YourModelName/src/reducers/to-do-list.ts` (or similar, based on your model name). - Implement the functions for each operation (e.g., `addTodoItemOperation`, `updateTodoItemOperation`). - These functions take the current state and an action (containing input data) and return the new state. - Powerhouse handles immutability behind the scenes. ### 3.2. Write unit tests for reducers It's crucial to test your reducer logic. Write unit tests in the `document-models/YourModelName/src/reducers/tests/` directory. - Verify that each operation correctly transforms the document state. - Use the auto-generated action creators from the `gen/` folder to create operation actions for your tests. Run tests using: ```bash pnpm run test ``` Or with npm: ```bash npm test ``` ### 3.3. Test the editor Test your editor in Vetra Studio by creating a new document of your defined type. Interact with your editor, test all functionalities, and ensure it correctly dispatches actions to the reducers and reflects state changes.
Alternatively: Use Connect Run Connect locally to see your editor in action: ```bash ph connect ``` Create a new document of your defined type. Interact with your editor, test all functionalities, and ensure it correctly dispatches actions to the reducers and reflects state changes.
**TIP:** **Working with MCP and Claude** 1. Provide clear, specific instructions. 2. Ask for clarifying questions to be answered before code generation. 3. Review generated schemas before confirmation. 4. Work in layers instead of 'one-shotting' your code. 5. Verify implementation details in generated code before continuing. 6. Always double-check proposed next actions.
Complete Guide: Tips for Working with Claude in Vetra Studio ## Before You Start **Setup Requirements:** 1. Run `ph vetra --interactive --watch` in one terminal first 2. Start Claude in a separate terminal from your project directory 3. Connect with: `claude` or `connect to the reactor mcp` 4. Verify you see the confirmation message with your drive name ## Communication Best Practices ### 1. Always Review Before Implementation **CRITICAL**: Claude will present a proposal before creating anything. You'll see: - Proposed document model structure (state schema, operations, modules) - How data will be organized - What actions users can perform **Always review and confirm** before Claude proceeds. This is your chance to adjust the design. ### 2. Be Specific and Detailed When describing what you need, include: **For Document Models:** - Purpose of the document (what problem does it solve?) - All data fields and their types (strings, numbers, dates, etc.) - What operations users should be able to perform - Any relationships between data - Business rules or constraints **For Document Editors:** - Which document model it's for - UI layout and components you want - User interactions and workflows - Specific operations to use (by name from your document model) - Any styling preferences ### 3. Use Clear Examples Good prompt for a document model: ``` Create a document model for expense tracking with: - Each expense has: amount (number), description (text), category (expense type), date, and receipt URL (optional) - Users can: add expenses, edit expense details, delete expenses, and categorize by type - Track total amount automatically ``` Good prompt for an editor: ``` Create an editor for the expense tracker with: - A form to add new expenses (amount, description, category, date) - A table showing all expenses with sort by date - Each row has edit and delete buttons - Show total at the bottom - Use the addExpense, updateExpense, and deleteExpense operations ``` ### 4. Work in Layers (Don't "One-Shot") Instead of asking for everything at once: - โœ… Start with the core document model - โœ… Test it works - โœ… Then add the editor - โœ… Then add advanced features This approach catches issues early and gives you better results. ### 5. Interactive Mode Benefits Using `ph vetra --interactive` gives you confirmation prompts: - Schema changes - Operation definitions - Code generation **Review each step** before confirming - it's easier to adjust now than later. ### 6. What to Expect After Implementation Claude will automatically: - Run TypeScript checks (`npm run tsc`) - Run linting (`npm run lint:fix`) - Report any errors found - Fix issues if needed You'll see confirmation when everything compiles successfully. ### 7. Common Issues and How to Avoid Them **Issue**: Generated model doesn't match expectations - **Solution**: Provide more detailed requirements upfront. Ask clarifying questions. **Issue**: Operations don't work as expected - **Solution**: Be explicit about all actions and their parameters. Use real-world examples. **Issue**: Editor UI doesn't look right - **Solution**: Describe the UI in detail (layout, components, interactions). Reference similar interfaces if helpful. ## Key Concepts to Know - **Document Model**: The template/blueprint for your documents (like a database schema) - **Document**: An actual instance with real data (like a database record) - **Operations**: Actions users can perform (like "add expense", "update status") - **Editor**: The user interface to interact with your documents - **Drive**: A collection that holds your documents (like a folder) ## Quick Tips 1. **Be specific**: More detail = better results 2. **Review proposals**: Always check before confirming 3. **Work incrementally**: Build in layers, not all at once 4. **Use operation names**: Reference them when describing editor functionality 5. **Ask questions**: If unsure, ask Claude to clarify or suggest options 6. **Test as you go**: Create model first, test it, then add the editor ## What Claude Can Do For You - Generate complete document models from natural language - Create all necessary operations automatically - Build React editor interfaces with your specifications - Handle all the TypeScript and boilerplate code - Fix type errors and linting issues - Add demo documents to test your models ## What You Should Focus On - Clearly describing your business requirements - Defining what data you need to track - Specifying what actions users should perform - Reviewing and confirming proposals - Testing the generated results **Remember**: Claude works best with clear, detailed requirements. Take time to explain what you want - it's faster than multiple iterations to fix misunderstandings.
## Phase 4: Packaging and publishing Once your document model and editor are implemented and tested, you can package them for distribution. A Powerhouse Package is a modular unit that can group document models, editors, scripts, and processors. ### 4.1. Prepare project for packaging If you didn't initialize your project with `ph init` intending it as a package, ensure your project structure is set up correctly. The `ph init` command is designed to create this structure. - `document-models/`: Contains your document models. - `editors/`: Contains your editor components. - `src/`: Often used for shared utilities or can be part of the model/editor structure. - (Optional) `processors/`, `scripts/` for advanced functionalities. ### 4.2. Specify package details in `package.json` Navigate to the `package.json` file in your project root. This file is crucial for NPM publishing. - **`name`**: Follow a scoped naming convention, e.g., `@your-org-ph/your-package-name`. The `-ph` suffix helps identify Powerhouse ecosystem packages. - **`version`**: Use semantic versioning (e.g., `1.0.0`). - **`author`**: Your name or organization. - **`license`**: e.g., `AGPL-3.0-only`. - **`main`**: The entry point of your package (e.g., `index.js` or `dist/index.js`). - **`publishConfig`**: For scoped packages intended to be public, add: ```json "publishConfig": { "access": "public" } ``` Example `package.json` snippet: ```json { "name": "@your-org-ph/your-package-name", "version": "1.0.0", "author": "Your Name", "license": "AGPL-3.0-only", "main": "index.js", "files": [ // Ensure your build output and necessary files are included "dist", "manifest.json", "document-models", "editors" ], "publishConfig": { "access": "public" } } ``` ### 4.3. Add a manifest file (`manifest.json`) Create a `manifest.json` file in your project root. This file describes your package's contents (document models, editors) and helps host applications like Connect understand and integrate your package. Example `manifest.json`: ```json { "name": "@yourorg-ph/your-package-name", // it's recommended to use an organization-specific NPM account (e.g., `yourorg-ph`). "description": "A brief description of your package and its document models.", "category": "your-category", // e.g., "Finance", "People Ops", "Legal" "publisher": { "name": "your-publisher-name", // Your organization or name "url": "your-publisher-url" // Link to your website or repository }, "documentModels": [ { "id": "powerhouse/YourModelName", // Document type string as defined in Connect "name": "YourModelName" // Human-readable name } ], "editors": [ { "id": "your-editor-id", // A unique ID for the editor component "name": "YourModelName Editor", // Human-readable name "documentTypes": ["powerhouse/YourModelName"] // Links editor to document type(s) } ] } ``` Update your project's main `index.js` or entry point to export your document models and editors so they can be discovered by Powerhouse applications. ### 4.4. Build your project Compile and optimize your project for production: ```bash ph build ``` This command typically creates a `dist/` or `build/` directory with the compiled assets. Ensure your `package.json`'s `files` array includes this directory and other necessary assets like `manifest.json`, `document-models`, and `editors` if they are not part of the build output but need to be in the package. ### 4.5. Version control Store your project in a Git repository for versioning and collaboration. ```bash git init git add . git commit -m "Initial commit of document model package" # git remote add origin # git push -u origin main ``` ### 4.6. Publish to NPM To share your package with others or deploy it to different environments, publish it to the NPM registry. 1. **Login to NPM:** If you haven't already, log into your NPM account. It's recommended to use an organization-specific NPM account (e.g., `yourorg-ph`). ```bash npm login ``` Follow the prompts in your terminal or browser. 2. **Publish the package:** ```bash ph publish ``` This publishes your package to the Vetra registry, making it available to install in any Vetra Cloud environment. ### 4.7. Using your published package Once published, your package can be installed and used in any Powerhouse environment (like another local Connect instance or a deployed Reactor setup). ```bash ph install @your-org-ph/your-package-name ``` This command makes the document models and editors defined in your package available within that Powerhouse instance. Congratulations! You've successfully created, packaged, and published a Powerhouse Document Model. This enables modularity, reusability, and collaboration within the Powerhouse ecosystem. --- ## Vetra Cloud > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/VetraCloud Vetra Cloud is the hosted infrastructure layer for Powerhouse applications. It lets you spin up a personal cloud environment that runs **Powerhouse Connect** (the user-facing document editor) and **Powerhouse Switchboard** (the backend GraphQL server) โ€” and extend them with packages from the Vetra registry. --- ## Getting Started ### 1. Connect Your Wallet Navigate to the **Cloud** section in Vetra and authenticate with your Ethereum wallet via **Renown**. Renown lets Connect sign operations on behalf of your on-chain identity without exposing your private key.
Connect Wallet via Renown
Connect your Ethereum wallet via Renown to authenticate with Vetra.
--- ### 2. Create an Environment After authenticating, create a new environment by giving it a name. Each environment gets its own subdomain (e.g. `my-env.vetra.io`) and runs its own isolated set of services.
Create Environment
Give your environment a name to get a dedicated subdomain and isolated set of services.
--- ### 3. Configure Services Inside your environment you can enable and configure three services: | Service | Description | | -------------------------- | -------------------------------------------------------------- | | **Powerhouse Connect** | The document-editor UI served at `connect..vetra.io` | | **Powerhouse Switchboard** | The GraphQL API server at `switchboard..vetra.io/graphql` | | **Powerhouse Fusion** | Optional data-fusion layer (disabled by default) | Use the **Size** dropdown to pick the compute tier for each service and toggle it on or off with the switch on the right.
Services overview
Enable and configure Connect, Switchboard, and Fusion services for your environment.
--- ### 4. Select a Version Each service shows its currently pinned version. Click **Change version** (or **Update All** when updates are available) to pin Connect and Switchboard to a specific release.
Available updates
Pin each service to a specific version or apply all available updates at once.
--- ### 5. Install Packages Packages extend Connect and Switchboard with additional document models, editors, and reactor modules. Click **+ Add package** in the **Installed Packages** section, then search for a package by name. Available packages include community and official Powerhouse packages such as: - `@powerhousedao/builder-profile` - `@powerhousedao/contributor-billing` - `@powerhousedao/knowledge-note` - `@arbitrum/arbgrants`
Add Package
Search and select a package from the Vetra registry to install into your environment.
Once you select a package, a **Pending change** banner appears at the bottom. Click **Approve** to apply the change and redeploy your environment. --- ## Next Steps Once your environment is running you can point your local `ph` CLI at it, or share the Connect URL with collaborators so they can start working with your document models right away. To publish your own packages to the registry, see the [Publishing Packages](../docs/02-PublishingPackages/index.md) guide. --- ## Vetra builder tooling > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/BuilderTools This page provides an overview of all the builder tooling offered in the Vetra ecosystem by Powerhouse. This list will be maintained and updated as our toolkit grows. ## Powerhouse command line interface --- ### Installing the Powerhouse CLI **TIP:** The Powerhouse CLI tool is the only essential tool to install on this page. Once you've installed it with the command below you can continue to the next steps. The Powerhouse CLI (`ph-cmd`) is a command-line interface tool that provides essential commands for managing Powerhouse projects. You can get access to the Powerhouse ecosystem tools by installing them globally using: ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` Key commands include: - `ph connect` for running the Connect application locally - `ph switchboard` or `ph reactor` for starting the API service - `ph init` to start a new project and build a Document Model - `ph help` to get an overview of all the available commands This tool will be fundamental on your journey when creating, building, and running Document Models
How to make use of different branches? When installing or using the Powerhouse CLI commands you are able to make use of the dev & staging branches. These branches contain more experimental features then the latest stable release the PH CLI uses by default. They can be used to get access to a bugfix or features under development. | Command | Description | | ----------------------------------------------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** or **npm install -g ph-cmd ** | Install latest stable version | | **pnpm install -g ph-cmd@dev** or **npm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** or **npm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
How to clean your system of the Powerhouse CLI? ### Cleaning and updating ph-cmd If you need to perform a clean reinstallation of the Powerhouse CLI (`ph-cmd`), follow these steps: 1. First, uninstall the global ph-cmd package: ```bash pnpm uninstall -g ph-cmd # or with npm npm uninstall -g ph-cmd ``` 2. Remove the Powerhouse configuration directory: ```bash rm -rf ~/.ph ``` 3. Reinstall the CLI tool (choose one): ```bash # For the latest stable version pnpm install -g ph-cmd # or with npm npm install -g ph-cmd # For the staging version pnpm install -g ph-cmd@staging # or with npm npm install -g ph-cmd@staging # For a specific version pnpm install -g ph-cmd@ # or with npm npm install -g ph-cmd@ ``` This process ensures a clean slate by removing both the CLI tool and its configuration files before installing the desired version. It's particularly useful when: - Troubleshooting CLI issues - Upgrading to a new version - Switching between stable and staging versions - Resolving configuration conflicts
### The use command The use command allows you to switch between different environments for your Powerhouse project dependencies. ```bash ph use ``` **Available Tags** - latest - Uses the latest stable version of all Powerhouse packages. - dev - Uses development versions of the packages. - staging - Uses staging versions of the packages. **Examples** #### Switch to latest stable versions ```bash ph use latest ``` #### Switch to development versions ```bash ph use dev ``` #### Use local monorepo packages from a specific path ```bash ph use-local /path/to/local/packages ``` #### Use a specific package manager ```bash ph use latest --package-manager pnpm ``` ### The update command The update command allows you to update your Powerhouse dependencies to their latest versions based on the version ranges specified in your package.json. ```bash ph update [options] ``` **Examples** #### Update dependencies based on package.json ranges ```bash ph update ``` #### Force update to latest dev versions ```bash ph update --force dev ``` #### Force update to latest stable versions ```bash ph update --force prod ``` #### Use a specific package manager ```bash ph update --package-manager pnpm ``` ## **Key differences** ### **Use command** - For switching between different **environments**. - Requires you to specify an environment. - Can work with **local packages**. ### **Update command** - Updating **dependencies** within your current environment. - Optional with its parameters. - Focused on updating **remote package** versions. Both commands support multiple package managers (npm, yarn, pnpm, and bun) and will automatically detect your project's package manager based on the lockfile present in your project directory. ## Boilerplate --- The Document Model Boilerplate is a foundational template that is used for code generation when scaffolding your editors and models. It ensures compatibility with host applications like Connect and Switchboard for seamless Document Model and editor integration. After installing `ph-cmd`, you will run `ph init` to initialize a project directory and structure. This initialization command makes use of the boilerplate. The boilerplate includes essential commands with NPM/PNPM scripts for: - Generating code - Linting - Formatting - Building - Testing ## Design system --- The Powerhouse Design System is a collection of reusable front-end components based on GraphQL scalars, including custom scalars specific to the web3 ecosystem. It provides: - Consistent UI components across Powerhouse applications - Automatic inclusion as a dependency in new Document Model projects - Customization options using CSS variables We cover some of these topics in our design system documentation. Read more about the [design system here](/academy/ComponentLibrary/DocumentEngineering) ## Reactor libraries --- Reactors are the nodes in the Powerhouse network that handle document storage, conflict resolution, and operation verification. The Reactor Libraries include: ### API - **Subgraphs**: Modular GraphQL services that connect to the Reactor for structured data access - **Processors**: Event-driven components that react to document changes and process data ### Browser Handles client-side interactions ### Local Manages local storage and offline functionality ### drive-app Handles document organization and storage management, but can also be customized to offer specific functionality, categorization, or tailored interfaces for your documents. ## Code generators --- Powerhouse provides several code generation tools to streamline development: ### Document model scaffolding Generates the basic structure for new Document Models with the command `ph init` based on the boilerplate. ### Editor generator Creates template code for Document Model editors with the command `ph generate editor --name --document-type ` ### Subgraph generator Creates GraphQL subgraph templates for data access automatically upon `ph reactor` ### Processor generator Generates processor templates for event handling automatically upon `ph reactor` ### Analytics processor generator Creates specialized processors for analytics tracking ## Analytics engine --- The Analytics Engine is a system that allows tracking and analyzing operations and state changes on Document Models. Features include: - Custom dashboard and report generation - Document Model-specific analytics - Metric and dimension tracking - Data combination from multiple Document Models Generate an analytics processor using: ```bash ph generate processor --type analytics ``` --- ## What is a document model? > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel **TIP:** This chapter on **Document Model Creation** will help you with an in-depth practical understanding while building an **advanced to-do list** document model. If you have completed the [Get Started](/academy/GetStarted/CreateNewPowerhouseProject) tutorial (which includes creating a simple to-do list document model), you will revisit familiar topics and can enhance your existing document model with the advanced features covered in this Mastery Track, such as statistics tracking. Although not required, completing the Get Started tutorial first is highly recommended as it provides a hands-on foundation for the concepts explored in depth here. **INFO:** A Document Model is a programmable document structure that defines how data is stored, changed, and interpreted in a decentralized system. It acts like a living blueprintโ€”capturing state, tracking changes, and enabling interaction through defined operations. For instance, an invoice document model might define fields like _issuer_, _lineItems_, and _status_, with operations such as _AddLineItem_ and _MarkAsPaid_. Document models are central to **Specification Driven Design & Development**โ€”an approach where you communicate your solution and intent through structured specification documents. These specs are machine-readable and executable, serving as a shared language between developers, designers, and AI agents. This enables precise, iterative development and lays the groundwork for AI-assisted workflows. A Document Model can be understood as: - A structured software framework that represents and **manages business logic** within a digital environment. - A sophisticated template that **encapsulates the essential aspects of a digital process or a set of data**. - A blueprints that define how data is **captured, manipulated, and visualised** within a system. - A **standardized way to store, modify, and query data** in scalable, decentralized applications. ### **How does a document model function?** #### **Structure and composition** Each document model consists of three key components: 1. **State Schema** โ€“ Defines the structure of the document. 2. **Document Operations** โ€“ Defines how the document can be modified. 3. **Event History** โ€“ Maintains an append-only log of changes. Document models leverage **event sourcing, CQRS (Command Query Responsibility Segregation), and an append-only architecture** to ensure immutability, auditability, and scalability. --- ## **1. Structure of a document model** A document model consists of the following key components: ### **1.1 State schema** The **state schema** defines the structure of the document, including its fields and data types. It serves as a blueprint for how data is stored and validated. Example of a **GraphQL-like state schema** for an invoice document: ```graphql type InvoiceState { id: OID! # Unique identifier for the invoice issuer: OID! # Reference to the issuing entity recipient: OID! # Reference to the recipient entity status: String # (value: "DRAFT") # Invoice status dueDate: DateTime # Payment due date lineItems: [LineItem!]! # List of line items totalAmount: Currency # Computed field for total invoice value } type LineItem { id: OID! description: String quantity: Int unitPrice: Currency } ``` ### **State schema features:** - Uses **GraphQL-like definitions** for a **clear, structured schema**. - Supports **custom scalar types** like `OID`, `Currency`, and `DateTime`. Or other Web3 specific scalars - Defines **relationships** using object references (`OID!`). The state schema acts as a **template** for document instances. Every new invoice created will follow this structure. --- ### **1.2 Document operations** Document models are **append-only**, meaning changes are not made directly to the document state. Instead, **document operations** define valid state transitions. Example operations for modifying an invoice: ```graphql input AddLineItemInput { invoiceId: OID! description: String quantity: Int unitPrice: Currency } input UpdateRecipientInput { invoiceId: OID! newRecipient: OID! } input MarkAsPaidInput { invoiceId: OID! } ``` Each operation **modifies the document state** without altering past data. Instead, a new event is appended to the document history. --- ### **1.3 Event history (append-only log)** Every operation applied to a document is **stored as an event** in an append-only log. #### **Example event log for an invoice document:** ```json [ { "timestamp": 1700000001, "operation": "CREATE_INVOICE", "data": { "id": "inv-001", "issuer": "company-123", "recipient": "client-456" } }, { "timestamp": 1700000100, "operation": "ADD_LINE_ITEM", "data": { "description": "Software Development", "quantity": 10, "unitPrice": 100 } }, { "timestamp": 1700000200, "operation": "MARK_AS_PAID", "data": {} ``` ### **Event history benefits:** - Provides a **transparent audit trail** of changes. - Enables **time travel debugging** by reconstructing past states. - Supports **event sourcing**, allowing developers to **replay events** to restore state. --- ## **2. How document models work technically** Document models in Powerhouse rely on **event-driven architecture, event sourcing, and CQRS principles**. Here's a step-by-step breakdown: ### **2.1 Document creation** 1. A user (or system) **submits an operation** to create a new document. 2. The document model **validates** the input data against the state schema. 3. The system **appends the operation** as an event in the document history. 4. The **initial state is computed** by applying the recorded events. **Example:** ```json { "operation": "CREATE_INVOICE", "data": { "id": "inv-001", "issuer": "company-123", "recipient": "client-456" } } ``` --- ### **2.2 Document modification** 1. A user submits an **operation** (e.g., `ADD_LINE_ITEM`). 2. The **event is appended** to the document history. 3. The **state transition logic updates the computed state**. 4. The UI re-renders the updated document. **Example:** Adding a line item: ```json { "operation": "ADD_LINE_ITEM", "data": { "description": "Software Development", "quantity": 10, "unitPrice": 100 } } ``` Since changes are **not applied directly**, this model is **highly scalable and auditable**. --- ### **2.3 Querying document models** Powerhouse uses **GraphQL queries** to fetch document states efficiently. Because documents store structured data, developers can instantly query: ```graphql query { invoice(id: "inv-001") { issuer recipient status lineItems { description quantity unitPrice } } } ``` This removes the need for **complex database joins** and allows for **fast, structured access to data**. --- ### **What do document models unlock?** Document Models offer a range of features that can be leveraged to create sophisticated, automated, and data-driven solutions: - **Automation**: Automate workflows using consistent, structured document logic. - **Auditability**: Maintain a full history of changes for compliance and transparency. - **API Integration**: Seamlessly connect with Switchboard or external APIs for data exchange. - **Data Analysis**: Enable real-time and historical insights through structured read models. - **Version Control**: Track and compare document states over time, similar to Git. - **Collaboration**: Empower decentralized teams to build, modify, and share documents asynchronously. - **Extensibility**: Add new fields, operations, and integrations over time without rewriting logic. - **Schema Evolution**: Evolve document models with [versioning](/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning)โ€”upgrade schemas while maintaining backward compatibility with existing documents. Document Models are a powerful primitive within the Powerhouse vision, offering a flexible, structured, and efficient way to manage business logic and data. ### Up next: How to build a document model In the next chapters, we'll teach you how to build a To-do List document model while explaining all of the theory that is involved. --- ## Specify the state schema > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema The state schema is the backbone of your document model. It defines the structure, data types, and relationships of the information your document will hold. In Powerhouse, we use the **GraphQL Schema Definition Language (SDL)** to define this schema. A well-defined state schema is crucial for ensuring data integrity, consistency, and for enabling powerful querying and manipulation capabilities. **TIP:** Your state schema is more than just a data structureโ€”it's a **specification** that enables **Specification Driven Design & Development**. This schema becomes a machine-readable blueprint that AI agents can interpret and execute, enabling precise collaboration between you and AI throughout the development process. ## Core concepts ### Types At the heart of GraphQL SDL are **types**. Types define the shape of your data. You can define custom object types that represent the entities in your document. For example, in a `TodoList` document, you might have a `TodoListState` type and a `TodoItem` type. - **`TodoListState`**: This could be the root type representing the overall state of the to-do list. It might contain a list of `TodoItem` objects. - **`TodoItem`**: This type would represent an individual to-do item, with properties like an `id`, `text` (the task description), and `checked` (a boolean indicating if the task is completed). ### Fields Each type has **fields**, which represent the properties of that type. Each field has a name and a type. For instance, the `TodoItem` type would have an `id` field of type `OID!`, a `text` field of type `String!`, and a `checked` field of type `Boolean!`. ### Scalars GraphQL has a set of built-in **scalar types**: - `String`: A UTFโ€8 character sequence. - `Int`: A signed 32โ€bit integer. - `Float`: A signed double-precision floating-point value. - `Boolean`: `true` or `false`. - `ID`: A unique identifier, often used as a key for a field. It is serialized in the same way as a String; however, it is not intended to be human-readable. In addition to these standard types, the Powerhouse Document-Engineering system introduces custom scalars that are linked to reusable front-end components. These scalars are tailored for the web3 ecosystem and will be explored in the Component Library section of the documentation. **TIP:** Powerhouse provides the `OID` (Object ID) scalar type, which is a custom scalar specifically designed for unique identifiers in document models. It provides automatic ID generation capabilities when used with the `generateId()` function from the document-model core library. ### Lists and non-null You can modify types using lists and non-null indicators: - **Lists**: To indicate that a field will return a list of a certain type, you wrap the type in square brackets, e.g., `[TodoItem!]!`. This means the field `items` in `TodoListState` will be a list of `TodoItem` objects. - **Non-Null**: To indicate that a field cannot be null, you add an exclamation mark `!` after the type name, e.g., `String!`. This means that the `text` field of a `TodoItem` must always have a value. The outer `!` in `[TodoItem!]!` means the list itself cannot be null (it must be at least an empty list), and the inner `!` on `TodoItem!` means that every item within that list must also be non-null. ## Example: TodoList state schema Let's revisit the `TodoList` example from the "Define the TodoList document specification" tutorial in Get Started. ### Basic schema (matching Get Started tutorial) This is the same schema you built in the Get Started tutorial: ```graphql # The state of our TodoList type TodoListState { items: [TodoItem!]! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } ``` ### Advanced schema (with statistics tracking) **INFO:** In this Mastery Track, we'll extend the basic schema with a `stats` field to demonstrate how you can add computed statistics to your document model. This is an **optional enhancement** that builds on the foundation from Get Started. ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` ### Breakdown: - **`TodoListState` type**: - `items: [TodoItem!]!`: This field defines that our `TodoListState` contains a list called `items`. - `[TodoItem!]`: This signifies that `items` is a list of `TodoItem` objects. - `TodoItem!`: The `!` after `TodoItem` means that no item in the list can be null. Each entry must be a valid `TodoItem`. - The final `!` after `[TodoItem!]` means that the `items` list itself cannot be null. It can be an empty list `[]`, but it cannot be absent. - `stats: TodoListStats!` _(advanced)_: Holds aggregated statistics about the to-do items. - **`TodoItem` type**: - `id: OID!`: Each `TodoItem` has a unique identifier using Powerhouse's custom `OID` scalar. This is crucial for referencing specific items, for example, when updating or deleting them. - `text: String!`: The textual description of the to-do item. It cannot be null, ensuring every to-do has a description. - `checked: Boolean!`: Indicates whether the to-do item is completed. It defaults to a boolean value (true or false) and cannot be null. - **`TodoListStats` type** _(advanced)_: This type holds the summary statistics for the to-do list. - `total: Int!`: The total count of all to-do items. This field must be an integer and cannot be null. - `checked: Int!`: The number of to-do items that are marked as completed. This must be an integer and cannot be null. - `unchecked: Int!`: The number of to-do items that are still pending. This also must be an integer and cannot be null. ## Best practices for designing your state schema 1. **Start Simple, Iterate**: Begin with the core entities and properties. You can always expand and refine your schema as your understanding of the document's requirements grows. 2. **Clarity and Explicitness**: Name your types and fields clearly and descriptively. This makes the schema easier to understand and maintain. 3. **Use Non-Null Wisely**: Enforce data integrity by using non-null (`!`) for fields that must always have a value. However, be mindful not to over-constrain if a field can genuinely be optional. 4. **Normalize vs. Denormalize**: - **Normalization**: Similar to relational databases, you can normalize your data by having distinct types and linking them via IDs. This can reduce data redundancy. For example, if you had `User` and `TodoItem` and wanted to assign tasks, you might have an `assigneeId` field in `TodoItem` that links to a `User`'s `id`. - **Denormalization**: Sometimes, for performance or simplicity, you might embed data directly. For instance, if user information associated with a to-do item was very simple and only used in that context, you might embed user fields directly in `TodoItem`. - The choice depends on your specific use case, query patterns, and how data is updated. 5. **Consider Future Needs**: While you shouldn't over-engineer, think a little about potential future enhancements. For example, adding a `createdAt: String` or `dueDate: String` field to `TodoItem` might be useful later. 6. **Root State Type**: It's a common pattern to have a single root type for your document state (e.g., `TodoListState`). This provides a clear entry point for accessing all document data. By carefully defining your state schema, you lay a solid foundation for your Powerhouse document model, making it robust, maintainable, and easy to work with. The schema dictates not only how data is stored but also how it can be queried and mutated through operations, which will be covered in the next section. ## Practical implementation: defining the state schema in Vetra Studio Now that you understand the concepts behind the state schema, let's put it into practice. This section will guide you through creating a document model specification for the TodoList example discussed above.
Tutorial: The state schema specification ### Prerequisites - You have a Powerhouse project set up. If not, please follow the [Create a new Powerhouse Project](../docs/../GetStarted/CreateNewPowerhouseProject) tutorial. - Vetra Studio is running. If not, navigate to your project directory in the terminal and run `ph vetra --watch`. ### Steps 1. **Create a New Document Model**: - With Vetra Studio open in your browser, you'll see the Vetra Studio Drive. - Click the **Document Models 'Add new specification'** button to create a new document model specification. 2. **Define Document Metadata**: - **Name**: Give your document model a descriptive name: `TodoList`. **Pay close attention to capitalization, as it influences our code generation.** - **Document Type**: In the 'Document Type' field, enter a unique identifier for this document type: `powerhouse/todo-list`. 3. **Specify the State Schema**: - In the code editor provided, you'll see a template for a GraphQL schema. - Replace the entire content of the editor with the advanced `TodoList` schema we've designed in this chapter: ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` 4. **Sync Schema and View Initial State**: - After pasting the schema, click the **'Sync with schema'** button. - This action processes your schema and generates an initial JSON state for your document model based on the `TodoListState` type. You can view this initial state, which helps you verify that your schema is structured correctly. For now, you can ignore the "Modules & Operations" section. We will define and implement the operations that modify this state in the upcoming sections of this Mastery Track. By completing these steps, you have successfully specified the data structure for the advanced TodoList document model. The next step is to define the operations that will allow users to interact with and change this state.
Alternatively: Define the state schema in Connect ### Prerequisites - You have a Powerhouse project set up. If not, please follow the [Create a new Powerhouse Project](../docs/../GetStarted/CreateNewPowerhouseProject) tutorial. - Connect is running. If not, navigate to your project directory in the terminal and run `ph connect`. ### Steps 1. **Create a New Document Model**: - With Connect open in your browser, navigate into your local drive. - At the bottom of the page in the 'New Document' section, click the `DocumentModel` button to create a new document model specification. 2. **Define Document Metadata**: - **Name**: Give your document model a descriptive name: `TodoList`. **Pay close attention to capitalization, as it influences our code generation.** - **Document Type**: In the 'Document Type' field, enter a unique identifier for this document type: `powerhouse/todo-list`. 3. **Specify the State Schema**: - In the code editor provided, you'll see a template for a GraphQL schema. - Replace the entire content of the editor with the advanced `TodoList` schema we've designed in this chapter: ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` 4. **Sync Schema and View Initial State**: - After pasting the schema, click the **'Sync with schema'** button. - This action processes your schema and generates an initial JSON state for your document model based on the `TodoListState` type. You can view this initial state, which helps you verify that your schema is structured correctly. For now, you can ignore the "Modules & Operations" section. We will define and implement the operations that modify this state in the upcoming sections of this Mastery Track. By completing these steps, you have successfully specified the data structure for the advanced TodoList document model. The next step is to define the operations that will allow users to interact with and change this state.
For a complete, working example, you can always have a look at the [Example TodoList Repository](/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository) which contains the full implementation of the concepts discussed in this Mastery Track. --- ## Specify document operations > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/SpecifyDocumentOperations In the previous section, we defined the state schema for our document model. Now, we turn our attention to a critical aspect of document model creation: **specifying document operations**. These operations are the heart of your document's behavior, dictating how its state can be modified. ## What are document operations? In Powerhouse, document models adhere to event sourcing principles. This means that every change to a document's state is the result of a sequence of operations (or events). Instead of directly mutating the state, you define specific, named operations that describe the intended change. For example, in our `TodoList` document model, operations might include: - `ADD_TODO_ITEM`: To add a new task. - `UPDATE_TODO_ITEM`: To modify an existing task (e.g., change its text or mark it as completed). - `DELETE_TODO_ITEM`: To remove a task. Each operation acts as a command that, when applied, transitions the document from one state to the next. The complete history of these operations defines the document's journey to its current state. ## Connecting operations to the schema In the "Define TodoList Document Model" chapter in the "Get Started" guide, we used GraphQL `input` types to define the structure of the data required for each operation. Let's revisit that: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` These `input` types are not just abstract definitions; they are the **specifications for our document operations**. - **`AddTodoItemInput`** specifies that to execute an `ADD_TODO_ITEM` operation, we only need the `text` for the new item. The `id` is automatically generated by the reducer using Powerhouse's `generateId()` function. - **`UpdateTodoItemInput`** specifies that for an `UPDATE_TODO_ITEM` operation, we need the `id` of the item to update, and optionally new `text` or a `checked` status. - **`DeleteTodoItemInput`** specifies that a `DELETE_TODO_ITEM` operation requires the `id` of the item to be removed. **TIP:** Notice that `AddTodoItemInput` only requires `text` โ€” not an `id`. This is because the ID is generated automatically in the reducer using `generateId()` from `document-model/core`. This ensures unique, consistent IDs and follows the pattern used in the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo). Vetra Studio uses these GraphQL input types when you define operations within a module (e.g., the `todos` module with operations `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`). ## Designing effective document operations Careful design of your document operations is crucial for a robust and maintainable document model. Here are some key considerations: ### 1. Granularity Operations should be granular enough to represent distinct user intentions or logical changes. - **Too coarse:** An operation like `MODIFY_TODOLIST` that takes a whole new list of items would be too broad. It would be hard to track specific changes and could lead to complex reducer logic. - **Too fine:** While possible, having separate operations like `SET_TODO_ITEM_TEXT` and `SET_TODO_ITEM_CHECKED_STATUS` might be overly verbose if these are often updated together. `UPDATE_TODO_ITEM` with optional fields offers a good balance. - **Just right:** The `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, and `DELETE_TODO_ITEM` operations for our `TodoList` are good examples. They represent clear, atomic changes. ### 2. Naming conventions Clear and consistent naming makes your operations understandable. A common convention is `VERB_NOUN` or `VERB_NOUN_SUBJECT`. - Examples: `ADD_ITEM`, `UPDATE_USER_PROFILE`, `ASSIGN_TASK_TO_USER`. - In our case: `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`. The name you provide in Vetra Studio (or Connect) (e.g., `ADD_TODO_ITEM`) directly corresponds to the operation type that will be recorded and that your reducers will handle. ### 3. Input types (payloads) The input type for an operation (its payload) should contain all the necessary information to perform that operation, and nothing more. - **Completeness:** If an operation needs a user ID to authorize a change, include it in the input. - **Conciseness:** Avoid including data that isn't directly used by the operation. - **Clarity:** Use descriptive field names within your input types. `action.input.text` is clearer than `action.input.t`. The GraphQL `input` types we defined earlier (`AddTodoItemInput`, `UpdateTodoItemInput`, `DeleteTodoItemInput`) serve precisely this purpose. They ensure that whoever triggers an operation provides the correct data in the correct format. ### 4. Immutability and pure functions While not specified in the operation definition itself, remember that the _implementation_ of these operations (the reducers) should treat state as immutable and behave as pure functions. The operation specification (input type) provides the data for these pure functions. ## Role in event sourcing and CQRS - **Events:** Each successfully executed operation is recorded as an event in the document's history. This history provides an audit trail and allows for replaying events to reconstruct state, which is invaluable for debugging and understanding how a document evolved. - **Commands:** Document operations are essentially "commands" in a Command Query Responsibility Segregation (CQRS) pattern. They represent an intent to change the state. The processing of this command (by the reducer) leads to one or more events being stored and the state being updated. ## From specification to implementation Specifying your document operations is the bridge between defining your data structure (the state schema) and implementing the logic that changes that data (the reducers). 1. **You define the state schema** (e.g., `TodoListState`, `TodoItem`). 2. **You specify the operations** that can alter this state, along with their required input data (e.g., `ADD_TODO_ITEM` with `AddTodoItemInput`). 3. **Next, you will implement reducers** for each specified operation. Each reducer will take the current state and an operation's input, and produce a new state. The generated code from `ph generate` (as seen in `03-ImplementOperationReducers.md`) will create a structure for your reducers based on the operations you specified in the Connect application (which, in turn, were based on your GraphQL input types). For example, the `TodoListTodosOperations` type generated by Powerhouse will expect methods corresponding to `addTodoItemOperation`, `updateTodoItemOperation`, and `deleteTodoItemOperation`. ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Implementation uses action.input which matches AddTodoItemInput }, updateTodoItemOperation(state, action) { // Implementation uses action.input which matches UpdateTodoItemInput }, deleteTodoItemOperation(state, action) { // Implementation uses action.input which matches DeleteTodoItemInput }, }; ``` ## Practical implementation: Defining operations in Vetra Studio Now that you understand the theory, let's walk through the practical steps of defining these operations for our `TodoList` document model within Vetra Studio.
Tutorial: Specifying TodoList operations Assuming you have already defined the state schema for the `TodoList` as covered in the previous section, follow these steps to add the operations: 1. **Create a Module for Operations:** Below the schema editor in Vetra Studio, find the input field labeled `Add module`. Modules help organize your operations. - Type `todos` into the field and press Enter. 2. **Add the `ADD_TODO_ITEM` Operation:** A new field, `Add operation`, will appear under your new module. - Type `ADD_TODO_ITEM` into this field and press Enter. - An editor will appear for the operation's input type. You need to define the data required for this operation. Paste the following GraphQL `input` definition into the editor: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } ``` :::info Notice we don't include `id` in the input โ€” the reducer will generate it automatically using `generateId()` from `document-model/core`. ::: 3. **Add the `UPDATE_TODO_ITEM` Operation:** - In the `Add operation` field again, type `UPDATE_TODO_ITEM` and press Enter. - Paste the corresponding `input` definition into its editor: ```graphql # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } ``` 4. **Add the `DELETE_TODO_ITEM` Operation:** - Finally, type `DELETE_TODO_ITEM` in the `Add operation` field and press Enter. - Paste its `input` definition: ```graphql # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` 5. **Review:** After adding all three operations, your document model specification in Vetra Studio is complete for now. You can see how each operation (`ADD_TODO_ITEM`, etc.) is now explicitly linked to an input type that defines its payload. Vetra Studio automatically tracks your document model specifications. In the next chapter, we will see how the generator is used to create code for our reducers.
Alternatively: Define operations in Connect Assuming you have already defined the state schema for the `TodoList` as covered in the previous section, follow these steps to add the operations in Connect: 1. **Create a Module for Operations:** Below the schema editor in Connect, find the input field labeled `Add module`. Modules help organize your operations. - Type `todos` into the field and press Enter. 2. **Add the `ADD_TODO_ITEM` Operation:** A new field, `Add operation`, will appear under your new module. - Type `ADD_TODO_ITEM` into this field and press Enter. - An editor will appear for the operation's input type. You need to define the data required for this operation. Paste the following GraphQL `input` definition into the editor: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } ``` :::info Notice we don't include `id` in the input โ€” the reducer will generate it automatically using `generateId()` from `document-model/core`. ::: 3. **Add the `UPDATE_TODO_ITEM` Operation:** - In the `Add operation` field again, type `UPDATE_TODO_ITEM` and press Enter. - Paste the corresponding `input` definition into its editor: ```graphql # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } ``` 4. **Add the `DELETE_TODO_ITEM` Operation:** - Finally, type `DELETE_TODO_ITEM` in the `Add operation` field and press Enter. - Paste its `input` definition: ```graphql # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` 5. **Review and Export:** After adding all three operations, your document model specification in Connect is complete for now. You can see how each operation (`ADD_TODO_ITEM`, etc.) is now explicitly linked to an input type that defines its payload. The next step in a real project would be to click the `Export` button to save this specification file. In the next chapter, we will see how this exported file is used to generate code for our reducers.
## Conclusion Specifying document operations is a foundational step in building robust and predictable document models in Powerhouse. By clearly defining the **"what" (the operation and its input)** before implementing the **"how" (the reducer logic)**, you create a clear contract for state transitions. This approach enhances type safety, testability, and the overall maintainability of your document model. In the next section, we will dive deeper into the implementation of the reducer functions for these specified operations. --- ## Use the Document Model Generator > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/UseTheDocumentModelGenerator When building document models with **Vetra Studio**, code generation happens automatically. As you add and update specification documents in your Vetra Studio Drive, Vetra monitors your changes and generates the necessary scaffolding in real-time. You'll receive updates directly in your terminal as Vetra processes your specifications. This article covers the **manual code generation method** using the `ph generate` commandโ€”an alternative approach that remains available when working with **Connect** and exported `.phd` specification files. While Vetra Studio is the recommended workflow for most developers, understanding the generator command provides useful context for how the scaffolding works under the hood. ## When to Use Manual Generation | Workflow | Code Generation | | ---------------- | -------------------------------------------------------------------------- | | **Vetra Studio** | Automaticโ€”Vetra watches your specifications and generates code as you work | | **Connect** | Manualโ€”Export a `.phd` file and run `ph generate` | If you're using Vetra Studio with `ph vetra --interactive`, you don't need to run any generation commands. Vetra handles everything for you, prompting for confirmation before processing changes. ## Prerequisites (Connect Workflow Only) If you're using the Connect workflow and need to manually generate code: 1. **Powerhouse CLI (`ph-cmd`) Installed:** The generator is part of the Powerhouse CLI. If you haven't installed it, refer to the [Builder Tools documentation](/academy/MasteryTrack/BuilderEnvironment/BuilderTools#installing-the-powerhouse-cli). 2. **Exported `.phd` File:** You must have exported your document model specification from Connect as a `.phd` file (e.g., `TodoList.phd`). ## The Generate Command The core command to invoke the Document Model Generator is: ```bash ph generate document-model --file ``` Replace `` with the actual filename of your exported document model specification. For instance, if your exported file is named `TodoList.phd`, the command would be: ```bash ph generate document-model --file TodoList.phd ``` When executed, this command reads and parses the specification file and generates a set of files and directories within your Powerhouse project. ## Understanding the Generated Artifacts Whether generated automatically by Vetra or manually via `ph generate`, the output structure is the same. Understanding these artifacts helps you work effectively with your document model. The generator creates a new directory specific to your document model, located at: `document-models//` For example, using `TodoList.phd` would result in a directory structure under `document-models/todo-list/`. Inside this directory, you will find: ### 1. Specification Files - **`todo-list.json`**: A JSON representation of your document model specification containing the parsed schema, operation definitions, document type, and metadata. - **`schema.graphql`**: The raw GraphQL Schema Definition Language (SDL) for both the state and operationsโ€”a human-readable reference of your schema. ### 2. The `gen/` Directory (Auto-Generated Code) This directory houses all code automatically generated from your specification. **Do not manually edit files within the `gen/` directory**โ€”they will be overwritten when the model is regenerated. Key files include: - **`types.ts`**: TypeScript interfaces derived from your GraphQL schema, including types for your document's state (e.g., `TodoListState`), complex types (e.g., `TodoItem`), and operation inputs (e.g., `AddTodoItemInput`). - **`creators.ts`**: Action creator functions for each operation. Instead of manually constructing action objects, you use functions like `addTodoItem({ text: 'Buy groceries' })`. - **`utils.ts`**: Utility functions including helpers to create initial document instances (e.g., `utils.createDocument()`). - **`reducer.ts`**: A TypeScript interface defining the expected shape of your reducer implementation. ### 3. The `src/` Directory (Your Implementation) This is where you write custom logic. Unlike `gen/`, these files are meant for manual editing. - **`reducers/`**: Contains skeleton reducer files (e.g., `todos.ts`) with function stubs for each operation that you implement with state transition logic. - **`tests/`**: Test files for your reducer logic. ## Versioned Document Models When your document model needs to evolve over timeโ€”adding new fields, operations, or changing behaviorโ€”you can use **versioning**. This allows multiple versions of the same document model to coexist, with automatic upgrade paths between them. ### Enabling Versioning Versioning is enabled by default. Simply run: ```bash ph generate document-model --file TodoList.phd ``` ### Versioned Folder Structure With versioning enabled, the generator creates a different structure: ``` document-models/ โ””โ”€โ”€ todo/ โ”œโ”€โ”€ v1/ # Version 1 code โ”‚ โ”œโ”€โ”€ gen/ # Auto-generated v1 types and utilities โ”‚ โ”œโ”€โ”€ src/reducers/ # V1 reducer implementations โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 1 โ”œโ”€โ”€ v2/ # Version 2 code โ”‚ โ”œโ”€โ”€ gen/ โ”‚ โ”œโ”€โ”€ src/reducers/ โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 2 โ”œโ”€โ”€ upgrades/ # Migration logic โ”‚ โ”œโ”€โ”€ versions.ts # Supported versions list โ”‚ โ”œโ”€โ”€ v2.ts # Upgrade reducer: v1 โ†’ v2 โ”‚ โ””โ”€โ”€ upgrade-manifest.ts โ””โ”€โ”€ document-models.ts # Exports all versions + manifests ``` ### Key Differences with Versioning | Standard Generation | Versioned Generation | | ------------------------------- | ----------------------------------------- | | Single `gen/` and `src/` folder | Separate `v1/`, `v2/` folders per version | | One reducer implementation | Version-specific reducers | | No upgrade logic | `upgrades/` folder with manifests | | Direct module export | Exports all versions + upgrade manifests | For comprehensive documentation on implementing versioning, including upgrade reducers and integration with Connect and Switchboard, see [Document Model Versioning](/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning). ## Benefits of Generated Scaffolding The generation processโ€”whether automatic via Vetra or manual via `ph generate`โ€”provides: 1. **Reduced Boilerplate:** Automates creation of type definitions, action creators, and utilities. 2. **Type Safety:** TypeScript types from your GraphQL schema catch errors at compile-time. 3. **Consistency:** Standardized project structure across document models. 4. **Accelerated Development:** Focus on business logic instead of foundational plumbing. 5. **Ecosystem Alignment:** Generated code integrates seamlessly with the Powerhouse ecosystem. 6. **Single Source of Truth:** Code stays synchronized with your specification. 7. **Version Support:** Optional versioning enables safe schema evolution over time. ## Practical Examples ### Using Vetra Studio (Recommended) When using Vetra Studio, code generation is automatic: 1. **Start Vetra in Interactive Mode:** ```bash ph vetra --interactive ``` 2. **Create Your Document Model:** Define your `TodoList` document model in the Vetra Studio Driveโ€”either manually or with AI assistance through Claude and the Reactor MCP. 3. **Watch the Terminal:** As you add specifications, Vetra automatically detects changes and generates scaffolding. In interactive mode, you'll be prompted to confirm before generation proceeds. 4. **Explore Generated Files:** Once complete, find your generated files at `document-models/todo-list/`: - `todo-list.json` and `schema.graphql`: Your model definition - `gen/`: Type-safe generated code - `src/reducers/todos.ts`: Skeleton reducer functions ready for implementation ### Using Connect (Alternative Method)
Tutorial: Manual Generation with Connect This approach is useful when working with Connect's Document Model Editor or when you need explicit control over the generation process. #### Prerequisites - **`TodoList.phd` file**: Your document model specification exported from Connect. #### Steps 1. **Place the Specification File in Your Project:** Navigate to your Powerhouse project root and copy your `TodoList.phd` file there. 2. **Run the Generator Command:** ```bash ph generate document-model --file TodoList.phd ``` 3. **Explore the Generated Files:** After the command completes, find the new directory at `document-models/todo-list/`: - `todo-list.json` and `schema.graphql`: The definition of your model - `gen/`: Type-safe generated code including `types.ts`, `creators.ts`, etc. - `src/`: Implementation skeleton, including `src/reducers/todos.ts` with empty functions for `addTodoItemOperation`, `updateTodoItemOperation`, and `deleteTodoItemOperation`
## Next Steps With your document model scaffolded, the next step is implementing the reducer logic in `document-models/todo-list/src/reducers/todos.ts`. Each reducer function takes the current state and action input, returning the new document state. Subsequently, write unit tests for your reducers to ensure they behave correctly. This cycle of defining, generating, implementing, and testing forms the core loop of document model development in Powerhouse. --- ## Implement document reducers > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentReducers ## The heart of document logic In our journey through Powerhouse Document Model creation, we've defined the "what" โ€“ the structure of our data ([State Schema](02-SpecifyTheStateSchema.md)) and the ways it can be changed ([Document Operations](03-SpecifyDocumentOperations.md)). We've also seen how the [Document Model Generator](04-UseTheDocumentModelGenerator.md) translates these specifications into a coded scaffold. Now, we arrive at the "how": implementing **Document Reducers**. Reducers are the core logic units of your document model. They are the functions that take the current state of your document and an operation (an "action"), and then determine the _new_ state of the document. They are the embodiment of your business rules and the engine that drives state transitions in a predictable, auditable, and immutable way. ## Recap: The journey to reducer implementation Before diving into the specifics of writing reducers, let's recall the preceding steps: 1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `TodoListState`, `TodoItem`). 2. **Document Operation Specification**: You defined the GraphQL `input` types that specify the parameters for each allowed modification to your document (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`). These were then associated with named operations (e.g., `ADD_TODO_ITEM`) in the Connect application. 3. **Code Generation**: You used `ph generate ` to create the necessary TypeScript types, action creators, and, crucially, the skeleton file for your reducers (typically `document-models//src/reducers/todos.ts`). This generated reducer file is our starting point. It will contain function stubs or an object structure expecting your reducer implementations, all typed according to your schema. ## What is a reducer? ### The core principles In the context of Powerhouse and inspired by patterns like Redux, a reducer is a **pure function** with the following signature (conceptually): `(currentState, action) => newState` Let's break down its components and principles: - **`currentState`**: This is the complete, current state of your document model instance before the operation is applied. It's crucial to treat this as **immutable**. - **`action`**: This is an object describing the operation to be performed. It typically has: - A `type` property: A string identifying the operation (e.g., `'ADD_TODO_ITEM'`). - An `input` property (or similar, like `payload`): An object containing the data necessary for the operation, matching the GraphQL `input` type you defined (e.g., `{ text: 'Buy groceries' }` for `AddTodoItemInput`). - **`newState`**: The reducer must return a _new_ state object representing the state after the operation has been applied. If the operation does not result in a state change, the reducer should return the `currentState` object itself. ### Key principles guiding reducer implementation: 1. **Purity**: - **Deterministic**: Given the same `currentState` and `action`, a reducer must _always_ produce the same `newState`. - **No Side Effects**: Reducers must not perform any side effects. This means no API calls, no direct DOM manipulation, no `Math.random()` (unless seeded deterministically for specific testing scenarios), and no modification of variables outside their own scope. Their sole job is to compute the next state. 2. **Immutability**: - **Never Mutate `currentState`**: You must never directly modify the `currentState` object or any of its nested properties. - **Always Return a New Object for Changes**: If the state changes, you must create and return a brand new object. If the state does not change, you return the original `currentState` object. - This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail. :::tip Powerhouse uses Immer.js Powerhouse uses **Immer.js** under the hood, which means you can write code that _looks like_ it's mutating the state directly (e.g., `state.items.push(...)`), but Immer ensures it results in an immutable update. This gives you the best of both worlds: readable code and immutable state. ::: 3. **Single Source of Truth**: The document state managed by reducers is the single source of truth for that document instance. All UI rendering and data queries are derived from this state. 4. **Delegation to specific operation handlers**: While you can write one large reducer that uses a `switch` statement or `if/else if` blocks based on `action.type`, Powerhouse's generated code typically encourages a more modular approach. You'll often implement a separate function for each operation, which are then combined into a main reducer object or map. The `ph generate` command usually sets up this structure for you. For example, in your `document-models/todo-list/src/reducers/todos.ts`, you'll find an object structure like this: ```typescript import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list"; export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Your logic for ADD_TODO_ITEM }, updateTodoItemOperation(state, action) { // Your logic for UPDATE_TODO_ITEM }, deleteTodoItemOperation(state, action) { // Your logic for DELETE_TODO_ITEM }, }; ``` The `TodoListTodosOperations` type is generated by Powerhouse and ensures your reducer object correctly implements all defined operations. The `state` and `action` parameters within these methods will also be strongly typed based on your schema. ## Implementing reducer logic: A practical guide Let's use our familiar `TodoList` example to illustrate common patterns. ### Basic implementation (matching Get Started) The basic implementation matches what you built in the Get Started tutorial: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Generate a unique ID for the new todo item const id = generateId(); // Add the new item to the state (Immer handles immutability) state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { // Find the item to update by its ID const item = state.items.find((item) => item.id === action.input.id); // Return early if item not found if (!item) return; // Update only the fields that are provided (partial update) item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { // Filter out the item with the matching ID state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` **INFO:** Notice that `addTodoItemOperation` uses `generateId()` from `document-model/core` to create a unique ID. This is the recommended pattern โ€” the ID is generated in the reducer, not passed from the UI. This ensures consistent, unique IDs across all operations. ### Advanced implementation (with statistics tracking) **INFO:** This section extends the basic reducers with statistics tracking, matching the advanced schema from the previous section. This demonstrates how to update computed/derived state alongside your primary data. For the advanced version with `stats`, we need to update the statistics whenever items are added, updated, or deleted: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Generate a unique ID for the new todo item const id = generateId(); // Update statistics state.stats.total += 1; state.stats.unchecked += 1; // Add the new item to the state state.items.push({ id, text: action.input.text, checked: false, // New items always start as unchecked }); }, updateTodoItemOperation(state, action) { // Find the specific item we want to update const item = state.items.find((item) => item.id === action.input.id); if (!item) { throw new Error(`Item with id ${action.input.id} not found`); } // Update text if provided if (action.input.text !== undefined) { item.text = action.input.text; } // Handle checked status changes and update stats if ( action.input.checked !== undefined && action.input.checked !== item.checked ) { if (action.input.checked) { state.stats.unchecked -= 1; state.stats.checked += 1; } else { state.stats.unchecked += 1; state.stats.checked -= 1; } item.checked = action.input.checked; } }, deleteTodoItemOperation(state, action) { // Find the item to determine its checked status for stats const item = state.items.find((item) => item.id === action.input.id); if (item) { // Update statistics state.stats.total -= 1; if (item.checked) { state.stats.checked -= 1; } else { state.stats.unchecked -= 1; } } // Remove the item from the list state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` ### Common patterns explained #### 1. Adding an item ```typescript addTodoItemOperation(state, action) { const id = generateId(); // Generate unique ID state.items.push({ ...action.input, id, checked: false }); } ``` - We use `generateId()` to create a unique identifier - We spread `action.input` to get the text, add the generated ID and default `checked: false` - With Immer, this "mutation" is actually immutable #### 2. Updating an item ```typescript updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; } ``` - We find the item by ID - We use nullish coalescing (`??`) to only update fields that were provided - This allows partial updates (e.g., just changing `checked` without touching `text`) #### 3. Deleting an item ```typescript deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); } ``` - We use `filter` to create a new array without the deleted item - Immer handles making this immutable ## Leveraging generated types As highlighted in [Using the Document Model Generator](04-UseTheDocumentModelGenerator.md), `ph generate` produces TypeScript types for your state (e.g., `TodoListState`, `TodoItem`) and the inputs for your operations (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`). **Always use these generated types in your reducer implementations!** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TypeScript knows action.input has { text: string } const id = generateId(); state.items.push({ id, text: action.input.text, checked: false }); }, // ... other reducers }; ``` Using these types provides: - **Compile-time safety**: Catch errors related to incorrect property names or data types before runtime. - **Autocompletion and IntelliSense**: Improved developer experience in your IDE. - **Clearer code**: Types serve as documentation for the expected data structures. ## Practical implementation: Writing the `TodoList` reducers Now that you understand the principles, let's put them into practice by implementing the reducers for our `TodoList` document model.
Tutorial: Implementing the TodoList reducers This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate TodoList.phd` to scaffold your document model's code. ### Implement the operation reducers Navigate to `document-models/todo-list/src/reducers/todos.ts`. The generator will have created a skeleton file. Replace its contents with the following logic. **Basic version (without stats):** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` **Advanced version (with stats):** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.stats.total += 1; state.stats.unchecked += 1; state.items.push({ id, text: action.input.text, checked: false, }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) { throw new Error(`Item with id ${action.input.id} not found`); } if (action.input.text !== undefined) { item.text = action.input.text; } if ( action.input.checked !== undefined && action.input.checked !== item.checked ) { if (action.input.checked) { state.stats.unchecked -= 1; state.stats.checked += 1; } else { state.stats.unchecked += 1; state.stats.checked -= 1; } item.checked = action.input.checked; } }, deleteTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (item) { state.stats.total -= 1; if (item.checked) { state.stats.checked -= 1; } else { state.stats.unchecked -= 1; } } state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ```
## Reducers and the event sourcing model Every time a reducer processes an operation and returns a new state, Powerhouse records the original operation (the "event") in an append-only log associated with the document instance. The current state of the document is effectively a "fold" or "reduction" of all past events, applied sequentially by the reducers. This is why purity and immutability are so critical: - **Purity** ensures that replaying the same sequence of events will always yield the exact same final state. - **Immutability** ensures that each event clearly defines a discrete state transition, making it easy to audit changes and understand the document's history. ## Conclusion Implementing document reducers is where you breathe life into your document model's specification. By adhering to the principles of purity and immutability, and by leveraging the type safety provided by Powerhouse's code generation, you can build predictable, testable, and maintainable business logic. These reducers form the immutable backbone of your document's state management, perfectly aligning with the event sourcing architecture that underpins Powerhouse. With your reducers implemented, your document model is now functionally complete from a data manipulation perspective. The next chapter covers how to write tests for this logic to ensure its correctness and reliability. --- ## Implement document model tests > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentModelTests ## Ensuring robustness and reliability In the previous chapter, we implemented the core reducer logic for our document model. Now, we reach a critical stage that underpins the reliability and correctness of our entire model: **Implementing Document Model Tests**. Testing is not an afterthought; it's an integral part of the development lifecycle, especially in systems like Powerhouse where data integrity and predictable state transitions are paramount. Well-crafted tests serve as a safety net, allowing you to refactor and extend your document model with confidence. This document provides a practical, hands-on tutorial for testing the `TodoList` document model reducers you have just built. ## Practical implementation: Writing and running the TodoList tests This tutorial assumes you have implemented the `TodoList` reducers as described in the previous chapter and that the code generator has created a test file skeleton at `document-models/todo-list/src/tests/todos.test.ts`.
Tutorial: Implementing and running the TodoList reducer tests ### 1. Implement the reducer tests With the reducer logic in place, it's critical to test it. Navigate to the generated test file at `document-models/todo-list/src/tests/todos.test.ts` and replace its contents with comprehensive tests. This suite tests each operation, verifying not only that the `items` array is correct, but also that the operation itself is recorded properly in the document's history. **Basic tests (matching Get Started):** ```typescript AddTodoItemInput, DeleteTodoItemInput, UpdateTodoItemInput, } from "todo-tutorial/document-models/todo-list"; reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, TodoItemSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newText = "new text"; input.text = newText; input.checked = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // Verify the state was updated correctly const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(newText); expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newChecked = !mockItem.checked; input.checked = newChecked; input.text = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(mockItem.text); expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock( DeleteTodoItemInputSchema(), ); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); // Verify the item was removed from state const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ``` **Advanced tests (with stats verification):** **INFO:** If you implemented the advanced version with statistics tracking, add these additional tests to verify the stats are updated correctly. ```typescript describe("Todos Operations with Stats", () => { it("should update stats when adding a todo item", () => { const document = utils.createDocument(); const input = { text: "Buy milk" }; const updatedDocument = reducer(document, addTodoItem(input)); expect(updatedDocument.state.global.items).toHaveLength(1); expect(updatedDocument.state.global.stats.total).toBe(1); expect(updatedDocument.state.global.stats.unchecked).toBe(1); expect(updatedDocument.state.global.stats.checked).toBe(0); }); it("should update stats when checking a todo item", () => { const document = utils.createDocument(); // Add an item first const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; // Now check it const updatedDocument = reducer( addedDocument, updateTodoItem({ id: itemId, checked: true }), ); expect(updatedDocument.state.global.stats.total).toBe(1); expect(updatedDocument.state.global.stats.unchecked).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(1); }); it("should update stats when deleting an unchecked todo item", () => { const document = utils.createDocument(); // Add an item const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; // Delete it const updatedDocument = reducer( addedDocument, deleteTodoItem({ id: itemId }), ); expect(updatedDocument.state.global.items).toHaveLength(0); expect(updatedDocument.state.global.stats.total).toBe(0); expect(updatedDocument.state.global.stats.unchecked).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(0); }); it("should update stats when deleting a checked todo item", () => { const document = utils.createDocument(); // Add and check an item const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; const checkedDocument = reducer( addedDocument, updateTodoItem({ id: itemId, checked: true }), ); // Delete it const updatedDocument = reducer( checkedDocument, deleteTodoItem({ id: itemId }), ); expect(updatedDocument.state.global.items).toHaveLength(0); expect(updatedDocument.state.global.stats.total).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(0); }); }); ``` ### 2. Run the tests Now, run the tests from your project's root directory to verify your implementation. ```bash pnpm run test ``` Or with npm: ```bash npm test ``` If all tests pass, you have successfully verified the core logic of your `TodoList` document model. This ensures that the reducers you wrote behave exactly as expected.
## Best practices for document model tests While the tutorial provides a concrete example, keep these general best practices in mind when writing your tests: - **Isolate Tests**: Each `it` block should ideally test one specific aspect or scenario. `beforeEach` is crucial for resetting state between tests. - **Descriptive Names**: Name your `describe` and `it` blocks clearly so they explain what's being tested. - **AAA Pattern (Arrange, Act, Assert)**: - **Arrange**: Set up the initial state and any required test data (e.g., using `utils.createDocument()` and defining `input` objects). - **Act**: Execute the operation by calling the `reducer` with an action from a `creator`. - **Assert**: Check if the outcome is as expected using `expect()`. - **Test Immutability**: A key assertion is to ensure the state is not mutated directly. You can check that a new array or object was created: `expect(newState.items).not.toBe(oldState.items);`. - **Cover Edge Cases**: Test what happens when an operation receives invalid input (e.g., trying to update an item that doesn't exist). Your test should confirm the reducer either throws an error or returns the state unchanged, depending on your implementation. - **Run Tests Frequently**: Integrate testing into your development workflow. Run tests after making changes to ensure you haven't broken anything. The `pnpm run test` (or `npm test`) command is your friend. ## Conclusion: The payoff of diligent testing Implementing comprehensive tests for your document model reducers is an investment that pays dividends in the long run. It leads to: - **Higher Quality Models**: More reliable and robust document models with fewer bugs. - **Increased Confidence**: Ability to make changes and refactor code without fear of breaking existing functionality. - **Easier Debugging**: When tests fail, they pinpoint the exact operation and scenario that's problematic. - **Better Collaboration**: Tests clarify the intended behavior of the document model for all team members. By following the tutorial and applying these best practices, you can build a strong suite of tests that safeguard the integrity and functionality of your document models. This diligence is a hallmark of a "Mastery Track" developer, ensuring that the solutions you build are not just functional but also stable, maintainable, and trustworthy. ## Up next In the next chapter of the Mastery Track - Building User Experiences you will learn how to implement an [editor](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) for your document model so you can see a simple user interface for the **TodoList** document model in action. For a complete, working example, you can always have a look at the [Example TodoList Repository](/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository) which contains the full implementation of the concepts discussed in this Mastery Track. --- ## Example: Todo-demo-package > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository **INFO:** The Todo-demo is maintained by the Powerhouse Team and serves as a reference for testing and introducing new features. It will be continuously updated alongside the accompanying documentation. https://github.com/powerhouse-inc/todo-demo There are several ways to explore this package: ### Option 1: Rebuild the Todo-demo The Todo-demo and repository are your main reference points during the Mastery Track. Follow the steps in the "Mastery Track โ€“ Document Model Creation" chapters to build along with the examples. Key patterns used in the repository: - **Naming convention**: `TodoList`, `TodoItem`, `TodoListState` (one word, PascalCase) - **Document type**: `powerhouse/todo-list` - **Module name**: `todos` - **ID generation**: Uses `generateId()` from `document-model/core` in the reducer - **Hooks**: Uses `useSelectedTodoListDocument` for state management in the editor ### Option 2: Clone and run the code locally The package includes: - The Document Model - Reducer Code - Reducer Tests - Editor Code - Drive-app Code You can clone the repository and run Vetra Studio to see all the code in action: ```bash git clone https://github.com/powerhouse-inc/todo-demo cd todo-demo pnpm install ph vetra --watch ```
Alternatively: Run with Connect ```bash git clone https://github.com/powerhouse-inc/todo-demo cd todo-demo pnpm install ph connect ```
### Option 3: Install the todo demo package in a (local) host app Alternatively, you can install this package in a Powerhouse project or in your deployed host apps: ```bash ph install @powerhousedao/todo-demo ``` ## Comparing Get Started vs Mastery Track | Aspect | Get Started | Mastery Track (Advanced) | | ------------------ | -------------------------- | ------------------------------------ | | Schema | Basic `items` array only | Includes `stats` object for tracking | | Reducer complexity | Simple CRUD operations | Includes statistics updates | | Editor | Component-based with hooks | Same approach + stats display | | Tests | Basic operation tests | Includes stats verification tests | Both approaches use the same naming conventions and patterns โ€” the Mastery Track simply extends the foundation with additional features to demonstrate more advanced concepts. --- ## Document Model Versioning > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning **TIP:** This chapter covers **advanced document model versioning**โ€”a system for evolving document schemas and operations while maintaining backward compatibility with existing documents. This is essential when your document models need to change over time in production environments. ## Why Versioning? Document models in Powerhouse are **event-sourced**. Once a document is created with a certain schema (v1), and operations are applied to it, you can't simply change the schema without breaking existing documents. Versioning solves this problem by allowing you to: - **Add new fields** to the state schema - **Add new operations** to the document model - **Modify reducer logic** for new documents - **Automatically upgrade** old documents to new versions when needed **INFO:** Document Model Versioning is a system that allows multiple versions of the same document model to coexist. Each version has its own schema, operations, and reducers. Documents created with older versions continue to work with their original reducers, while new documents use the latest version. Upgrade manifests define how to migrate documents between versions. --- ## How Versioning Works ### The Problem It Solves Consider a simple Todo document model: **Version 1 State:** ```graphql type TodoState { todos: [Todo!]! } ``` Now you want to add a `title` field to track the list's name: **Version 2 State:** ```graphql type TodoState { title: String todos: [Todo!]! } ``` Without versioning, existing v1 documents would break because they don't have a `title` field. With versioning: - V1 documents continue to work with the v1 reducer - New documents are created with v2 - V1 documents can be **upgraded** to v2 when needed ### Key Components Document model versioning consists of four key components: | Component | Purpose | | ----------------------- | ------------------------------------------------------------------ | | **Version Folders** | Separate `v1/`, `v2/` directories containing version-specific code | | **DocumentModelModule** | Each version exports a module with explicit `version` number | | **Upgrade Manifest** | Declares supported versions and upgrade paths | | **Upgrade Reducer** | Transforms document state from one version to another | --- ## Folder Structure When versioning is enabled, the document model generator creates a versioned folder structure: ``` document-models/ โ””โ”€โ”€ todo/ โ”œโ”€โ”€ v1/ # Version 1 โ”‚ โ”œโ”€โ”€ gen/ # Auto-generated code (DO NOT EDIT) โ”‚ โ”‚ โ”œโ”€โ”€ reducer.ts โ”‚ โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”‚ โ”œโ”€โ”€ types.ts # V1 TypeScript types โ”‚ โ”‚ โ””โ”€โ”€ ... โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ””โ”€โ”€ reducers/ # Your v1 reducer implementations โ”‚ โ”‚ โ””โ”€โ”€ todo-operations.ts โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 1 โ”‚ โ”œโ”€โ”€ v2/ # Version 2 โ”‚ โ”œโ”€โ”€ gen/ โ”‚ โ”‚ โ””โ”€โ”€ types.ts # V2 TypeScript types (includes 'title') โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ””โ”€โ”€ reducers/ โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 2 โ”‚ โ”œโ”€โ”€ upgrades/ # Migration logic โ”‚ โ”œโ”€โ”€ versions.ts # Supported versions list โ”‚ โ”œโ”€โ”€ v2.ts # Upgrade reducer: v1 โ†’ v2 โ”‚ โ””โ”€โ”€ upgrade-manifest.ts # Ties everything together โ”‚ โ””โ”€โ”€ document-models.ts # Exports all versions + manifests ``` --- ## Core Type Definitions Understanding the underlying types helps you implement versioning correctly: ### UpgradeTransition Defines a single version upgrade: ```typescript type UpgradeTransition = { toVersion: number; upgradeReducer: UpgradeReducer; description?: string; }; ``` ### UpgradeManifest Declares all supported versions and their upgrade paths: ```typescript type UpgradeManifest = { documentType: string; latestVersion: number; supportedVersions: TVersions; upgrades: { // Keys are "v2", "v3", etc. (never "v1" - nothing to upgrade from) [V in Exclude, 1> as `v${V}`]: UpgradeTransition; }; }; ``` --- ## Implementation Guide ### Step 1: Enable Versioning in Code Generation Versioning is enabled by default when using the Powerhouse CLI: ```bash ph generate TodoList.phd ``` Or when using Vetra Studio, versioning support is configured in your project settings. ### Step 2: Version Configuration Files **versions.ts** - Declare supported versions: ```typescript // upgrades/versions.ts export const supportedVersions = [1, 2] as const; export const latestVersion = supportedVersions[1]; // 2 ``` ### Step 3: Document Model Module Each version exports a `DocumentModelModule` with an explicit version number: ```typescript // v1/module.ts export const Todo: DocumentModelModule = { version: 1, // Explicit version number reducer, actions, utils, documentModel: createState(defaultBaseState(), documentModel), }; ``` ```typescript // v2/module.ts export const Todo: DocumentModelModule = { version: 2, // Different version reducer, actions, utils, documentModel: createState(defaultBaseState(), documentModel), }; ``` ### Step 4: Upgrade Reducer The upgrade reducer transforms a document from one version to the next: ```typescript // upgrades/v2.ts function upgradeReducer( document: PHDocument, action: Action, ): PHDocument { return { ...document, state: { ...document.state, global: { ...document.state.global, title: "", // Initialize the new field }, }, initialState: { ...document.initialState, global: { ...document.initialState.global, title: "", // Also in initial state }, }, }; } export const v2: UpgradeTransition = { toVersion: 2, upgradeReducer, description: "Add title field to global state", }; ``` ### Step 5: Upgrade Manifest Tie everything together in the manifest: ```typescript // upgrades/upgrade-manifest.ts export const upgradeManifest: UpgradeManifest = { documentType: "my-org/todo", latestVersion, supportedVersions, upgrades: { v2 }, }; ``` ### Step 6: Export All Versions ```typescript // document-models.ts export const documentModels: DocumentModelModule[] = [TodoV1, TodoV2]; export const upgradeManifests: UpgradeManifest[] = [ todoUpgradeManifest, ]; ``` --- ## Integration with Connect and Switchboard ### How Connect Loads Versioned Documents Connect automatically loads all document model versions and upgrade manifests from your Vetra packages: ```typescript // Simplified view of Connect's reactor setup const documentModelModules = vetraPackages.flatMap( (pkg) => pkg.modules.documentModelModules, ); const upgradeManifests = vetraPackages.flatMap((pkg) => pkg.upgradeManifests); const reactor = await createBrowserReactor( documentModelModules, upgradeManifests, renown, ); ``` ### Creating Documents at Specific Versions By default, new documents are created at the **latest version**. You can optionally specify a version: ```typescript // Create at latest version (default) const doc = await client.createEmpty("my-org/todo"); // doc.state.document.version === 2 (latest) // Create at specific version const v1Doc = await client.createEmpty("my-org/todo", { documentModelVersion: 1, }); // v1Doc.state.document.version === 1 ``` ### Querying Documents Documents can be queried regardless of version: ```typescript // Find all todo documents (both v1 and v2) const result = await client.find({ type: "my-org/todo" }); ``` --- ## Use Cases ### 1. Adding a New Field **Scenario:** Your Todo document needs a `title` field. **Solution:** 1. Create v2 with the new field in the state schema 2. Implement upgrade reducer that sets `title: ""` 3. New documents get v2; existing v1 documents can be upgraded ### 2. Adding New Operations **Scenario:** V1 has `ADD_TODO`, `REMOVE_TODO`. V2 adds `EDIT_TITLE`. **How it works:** - V2 module includes the new operation - V1 documents don't have access to `EDIT_TITLE` until upgraded - The upgrade manifest handles the migration ### 3. Changing Reducer Behavior **Scenario:** V2 items should include an `addedAt` timestamp. ```typescript // V1 reducer - no timestamp function v1StateReducer(state, action) { if (action.type === "ADD_ITEM") { return { ...state, global: { items: [ ...state.global.items, { id: action.input.id, name: action.input.name, }, ], }, }; } } // V2 reducer - adds timestamp field function v2StateReducer(state, action) { if (action.type === "ADD_ITEM") { return { ...state, global: { items: [ ...state.global.items, { id: action.input.id, name: action.input.name, addedAt: action.input.addedAt, // New field from input }, ], }, }; } } ``` --- ## Best Practices ### Upgrade Reducer Guidelines 1. **Always handle both `state` and `initialState`** - Both need to be migrated 2. **Provide sensible defaults** for new fields 3. **Never lose data** - Transform existing data, don't delete it 4. **Keep upgrade reducers pure** - No side effects or async operations ### Version Compatibility - **Don't remove operations** from newer versions unless absolutely necessary - **Don't change existing operation input schemas** - add new operations instead - **Document breaking changes** in the upgrade transition description ### Testing Upgrades Test your upgrade reducers thoroughly: ```typescript it("should upgrade v1 document to v2", () => { const v1Doc = createV1Document(); v1Doc.state.global.todos = [{ id: "1", title: "Test", completed: false }]; const v2Doc = upgradeReducer(v1Doc, {} as Action); expect(v2Doc.state.global.title).toBe(""); // New field initialized expect(v2Doc.state.global.todos).toEqual(v1Doc.state.global.todos); // Data preserved }); ``` --- ## Summary | Concept | Description | | -------------------------- | ----------------------------------------------------------- | | **Version Folders** | `v1/`, `v2/` directories with version-specific code | | **DocumentModelModule** | Exports with explicit `version` field | | **UpgradeTransition** | Defines how to migrate from one version to the next | | **UpgradeManifest** | Declares all versions and upgrade paths for a document type | | **Backward Compatibility** | Old documents work with their original reducers | | **Automatic Upgrades** | Reactor handles version detection and migration | Document model versioning enables your applications to evolve safely while preserving the integrity of existing data. By following these patterns, you can confidently add new features, modify schemas, and improve your document models over time. --- ## Related Documentation - [Use the Document Model Generator](/academy/MasteryTrack/DocumentModelCreation/UseTheDocumentModelGenerator) - [Implement Document Reducers](/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentReducers) - [Powerhouse CLI Reference](/academy/APIReferences/PowerhouseCLI) --- ## Build document editors > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors ## Build with React on Powerhouse At Powerhouse, frontend development for document editors follows a simple and familiar flow, leveraging the power and flexibility of React. ### Development environment **Vetra Studio** is your primary tool for builder workflows and editor development. When you run `ph vetra --watch`, it provides a dynamic, local environment where you can define and preview your document models and their editors live. This replaces the need for tools like Storybook for editor development, though Storybook remains invaluable for exploring the [Powerhouse Component Library](#powerhouse-component-library). #### Key aspects of the Powerhouse development environment: - **React Foundation**: Build your editor UIs using React components, just as you would in any standard React project. - **Automatic Build Processes**: Tailwind CSS is installed by default and fully managed by Vetra Studio. There's no need to manually configure or run Tailwind or other build processes during development. Vetra Studio handles CSS generation and other necessary build steps automatically, especially when you publish a package. - **Styling Flexibility**: You are not limited to Tailwind. Regular CSS (`.css` files), inline styles, and any React-compatible styling method work exactly as you would expect. #### Powerhouse aims to keep your developer experience clean, familiar, and focused: - Build React components as you normally would. - Use styling approaches you're comfortable with. - Trust Vetra Studio to handle the setup and build processes for you.
Alternatively: Use Connect for development You can also use **Connect** as your development environment by running `ph connect`. Connect provides a similar dynamic local environment where you can preview your document models and their editors live. The development experience is essentially the same, with Connect also handling Tailwind CSS and build processes automatically.
### Generating your editor template When using **Vetra Studio**, editor generation is automatic and integrated into your workflow. Simply create an **Editor specification document** in your Vetra Studio Drive, and Vetra will automatically generate the editor template code for you. #### With Vetra Studio (Recommended) 1. Open Vetra Studio (`ph vetra --watch`) 2. In your Vetra Studio Drive, click **"Add new specification"** in the Editors section 3. Select your document model (e.g., `TodoList`) to link the editor to 4. Name your editor (e.g., `todo-list-editor`) 5. Vetra automatically generates the `editors/todo-list-editor/editor.tsx` template That's it! No manual commands needed. Vetra watches your specifications and generates code as you work.
Alternatively: Manual generation with ph generate If you're using Connect or prefer manual control, you can use the `ph generate` command to create an editor template: ```bash ph generate --editor todo-list-editor --document-types powerhouse/todo-list ``` This will create the template in the `editors/todo-list-editor/editor.tsx` folder. If you want a refresher on how to define your document model specification please read the chapter on [specifying the State Schema](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema)
### Styling your editor You have several options for styling your editor components: 1. **Default HTML Styling**: Standard HTML tags (`

`, `

`, ` ); } ``` **Why hooks are recommended:** - โœ… **Self-contained components**: Each component gets its own connection to the document - โœ… **Less boilerplate**: No need to pass props through multiple levels - โœ… **Easier refactoring**: Move components around without rewiring props - โœ… **Modern React pattern**: Follows React's recommended approach for state management ### Method 2: Using Props ๐Ÿ“ฆ The **props-based approach** receives the document and dispatch function as properties passed from a parent component. ```typescript export type IProps = EditorProps; export default function Editor(props: IProps) { const { document, dispatch } = props; const state = document.state.global; // Now you'd pass state and dispatch to child components as props return (

); } ``` **When props might be useful:** - When you need strict control over which components can access state - When building components that should work outside of Powerhouse context - For testing purposes where you want to inject mock state ### Which should you use? | Scenario | Recommended Approach | | ------------------------------------------------- | -------------------- | | Building a standard Powerhouse editor | **Hooks** ๐Ÿช | | Component needs document state | **Hooks** ๐Ÿช | | Building reusable UI components (buttons, inputs) | **Props** ๐Ÿ“ฆ | | Need to test components in isolation | **Props** ๐Ÿ“ฆ | **Bottom line**: Use hooks for most Powerhouse editor development. It's simpler, cleaner, and matches the patterns used in the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo). ### Additional hooks for editors Beyond the document-specific hooks (like `useSelectedTodoListDocument`), Powerhouse provides a comprehensive set of hooks from the reactor-browser package that you can use in your editors: | Hook | Description | | --------------------------- | --------------------------------------------- | | `useSelectedDocument` | Returns the currently selected document | | `useSelectedDocumentId` | Returns just the ID of the selected document | | `useDocumentById` | Returns a document by its ID | | `useSelectedDrive` | Returns the currently selected drive | | `useRevisionHistoryVisible` | Check and control revision history visibility | | `usePHModal` | Manage modals in your editor | **Example: Using `useDocumentById` to reference another document** ```typescript export function RelatedDocument({ documentId }: { documentId: string }) { const relatedDoc = useDocumentById(documentId); if (!relatedDoc) return Loading...; return (

Related: {relatedDoc.name}

{/* Display related document info */}
); } ``` **Example: Showing a modal from your editor** ```typescript export function CreateNewButton({ documentType }: { documentType: string }) { return ( ); } ``` For a complete list of all available hooks, see the [React Hooks API Reference](/academy/APIReferences/ReactHooks). ## Local vs. Global State When building editors, you'll work with two types of state: - **Global Document State**: Data that is part of the document itself and should be saved. This is accessed via hooks (`useSelectedTodoListDocument`) or props (`document.state.global`). You modify it by dispatching actions. - **Local Component State**: UI-specific state that doesn't need to be saved (e.g., "is the dropdown open?", "what's in the input field before submission?"). Use React's `useState` hook for this. ```typescript export function AddTodo() { // Local state - just for this component's UI const [inputValue, setInputValue] = useState(''); // Global document state - saved in the document const [todoList, dispatch] = useSelectedTodoListDocument(); const handleSubmit = () => { if (inputValue.trim()) { dispatch(addTodoItem({ text: inputValue })); // Updates global state setInputValue(''); // Clears local state } }; return (
setInputValue(e.target.value)} />
); } ``` ## Handling dispatch errors When dispatching actions to a document, you may want to handle errors that occur during action execution. The `dispatch` function accepts an optional `onErrors` callback as its second parameter, which is invoked with any errors thrown by the reducers when processing the actions. ```typescript useSelectedTodoListDocument, addTodoItem, } from "todo-tutorial/document-models/todo-list"; export function AddTodo() { const [todoList, dispatch] = useSelectedTodoListDocument(); const handleAdd = (text: string) => { dispatch(addTodoItem({ text }), (errors) => { // Handle errors - e.g., show a toast notification console.error("Failed to add todo:", errors); alert(`Error: ${errors[0]?.message}`); }); }; // ... rest of component } ``` This pattern is useful when you need to: - Display error messages to users - Log errors for debugging - Trigger recovery actions when an operation fails ## Powerhouse component library Powerhouse provides a rich set of reusable UI components through the **`@powerhousedao/document-engineering/scalars`** package. These components are designed for consistency, efficiency, and seamless integration with the Powerhouse ecosystem, with many based on GraphQL scalar types. For more information read our chapter on the [Component Library](/academy/ComponentLibrary/DocumentEngineering) ### Exploring components You can explore available components, see usage examples, and understand their properties (props) using our Storybook instance: [https://storybook.powerhouse.academy](https://storybook.powerhouse.academy) Storybook allows you to: - Visually inspect each component. - Interact with different states and variations. - View code snippets for basic implementation. - Consult the props table for detailed configuration options. ### Using components 1. **Import**: Add an import statement at the top of your editor file: ```typescript import { Checkbox, StringField, Form, } from "@powerhousedao/document-engineering/scalars"; ``` 2. **Implement**: Use the component in your JSX, configuring it with props: ```typescript // Example using StringField for an input
{ /* Handle submission */ }}> setTaskText(e.target.value)} /> ```
Tutorial: Implementing the TodoList Editor ## Build a TodoList editor In this final part of our tutorial we will continue with the interface or editor implementation of the **TodoList** document model. This means you will create a simple user interface for the **TodoList** document model which will be used inside the Connect app to create, update and delete your TodoList items, and also display the statistics we've implemented in our reducers (if you followed the advanced version). ## Generate the editor template ### Using Vetra Studio (Recommended) With Vetra Studio running (`ph vetra --watch`), simply create an Editor specification: 1. In your Vetra Studio Drive, click **"Add new specification"** in the Editors section 2. Select the **TodoList** document model to link the editor to 3. Name your editor `todo-list-editor` 4. Vetra automatically generates `editors/todo-list-editor/editor.tsx` Once complete, navigate to the `editors/todo-list-editor/editor.tsx` file and open it in your IDE.
Alternatively: Manual generation with ph generate If you're not using Vetra Studio, run the command below to generate the editor template: ```bash ph generate --editor todo-list-editor --document-types powerhouse/todo-list ``` This command reads the **TodoList** document model definition from the `document-models` folder and generates the editor template in the `editors/todo-list-editor` folder as `editor.tsx`. Notice the `--editor` flag which specifies the editor name, and the `--document-types` flag defines the document type `powerhouse/todo-list`.
### Editor implementation options When building your editor component within the Powerhouse ecosystem, you have several options for styling, allowing you to leverage your preferred methods: 1. **Default HTML Styling:** Standard HTML tags (`

`, `

`, ` ); } ``` ### Todo item component ```typescript // editors/todo-list-editor/components/Todo.tsx type Props = { todo: TodoItem; }; export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch(updateTodoItem({ id: todo.id, checked: event.target.checked })); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todo.id })); }; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; if (isEditing) { return (

); } return (
{todo.text}
); } ``` --- ## Advanced: Adding stats display **INFO:** If you implemented the advanced version with statistics tracking, you can add a stats component to display the todo counts. ```typescript // Add to TodoList.tsx export function TodoList() { const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const { items, stats } = selectedTodoList.state.global; return (

TodoList

{/* Stats section (only show if there are items) */} {items.length >= 2 && (
Total
{stats.total}
Completed
{stats.checked}
Remaining
{stats.unchecked}
)}
); } ``` --- ## Test your editor Now you can run Vetra Studio and see the **TodoList** editor in action: ```bash ph vetra --watch ``` In Vetra Studio, you'll be able to create and test your **TodoList** documents. Click on the Document Models section and create a new TodoList document. **TIP:** The editor will update dynamically, so you can play around with your editor styling while seeing your results appear in Vetra Studio.
Alternatively: Test with Connect You can also run the Connect app to see the **TodoList** editor in action: ```bash ph connect ``` In Connect, in the bottom right corner you'll find a new Document Model that you can create: **TodoList**. Click on it to create a new TodoList document. The editor will update dynamically, so you can play around with your editor styling while seeing your results appear in Connect.

Congratulations! If you managed to follow this tutorial until this point, you have successfully implemented the **TodoList** document model with its reducer operations and editor. ## Up Next Now you can move on to creating a [custom Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) for your TodoList document. Imagine you have many TodoLists sitting in a drive. A custom Drive-app will allow you to organize and track them at a glance, opening up a new world of possibilities to increase the functionality of your documents! ### Further Reading - [React Hooks API Reference](/academy/APIReferences/ReactHooks) โ€” Complete reference for all available Powerhouse hooks - [Component Library](/academy/ComponentLibrary/DocumentEngineering) โ€” Pre-built UI components for your editors --- ## Build a Drive-app > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer **Drive-apps** enhance how contributors and organizations interact with document models. They create an 'app-like' experience by providing a **custom interface** for exploring and interacting with the contents of a drive. **TIP:** A Drive-app offers a tailored application designed around its document models. Think of a Drive-app as a specialized lensโ€”it offers **different ways to visualize, organize, and interact with** the data stored within a drive, making it more intuitive and efficient for specific use cases. ### Drive-apps are purpose-built Organizations typically build Drive-apps for specific use cases, often packaging them with a corresponding document model. This allows for customized user experiences, streamlined workflows, and maximized efficiency for contributors. Drive-apps **bridge the gap between raw data and usability**, unlocking the full potential of document models within the Powerhouse framework. ### Key features of Drive-apps - **Custom Views & Organization** โ€“ Drive-apps can present data in formats like Kanban boards, list views, or other structured layouts to suit different workflows. - **Aggregated Insights** โ€“ They can provide high-level summaries of important details across document models, enabling quick decision-making. - **Enhanced Interactivity** โ€“ Drive-apps can include widgets, data processors, or read models to process and display document data dynamically. ## Build a Drive-app Drive-apps provide custom interfaces for interacting with the contents of a drive. Let's start with a **quick overview** of the steps for building a Drive-app. We will then apply these steps to create our **todo-list Drive-app**. ### Step 1. Generate the scaffolding code When using **Vetra Studio**, Drive-app generation is automatic. Simply create a **Drive-app specification document** in your Vetra Studio Drive: 1. Open Vetra Studio (`ph vetra --watch`) 2. In your Vetra Studio Drive, click **"Add new specification"** in the Apps section 3. Name your Drive-app (e.g., `todo-list-drive-explorer`) 4. Vetra automatically generates the Drive-app template code
Alternatively: Manual generation with ph generate If you're not using Vetra Studio, use the `generate drive editor` command to create the basic template structure: ```bash ph generate --drive-editor ```
### Step 2. Update the manifest file After creating your Drive-app, you need to update its `powerhouse.manifest.json` file. This file identifies your project and its components within the Powerhouse ecosystem. ### Step 3. Customize the Drive-app Review the generated template and modify it to better suit your document model: 1. Remove unnecessary files and components 2. Add custom views specific to your data model 3. Implement specialized interactions for your use case ### About the Drive-app template The default template provides a solid foundation. It contains: - A tree structure navigation panel - Basic file/folder operations - Standard layout components But the real power comes from tailoring the interface to your specific document models. Now, let's implement a specific example for the to-do list we've been working on throughout this guide. ## Implementation example: todo-list Drive-app This example demonstrates how to create a todo-list Drive-app application using the Powerhouse platform. The application allows users to create and manage to-do lists with a visual progress indicator. **WARNING:** If you've been following the Mastery Track, you can continue with the to-do list document model and Powerhouse project you've created. For more details, you can refer to the [Document Model Creation guide](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema). If not, you can follow the shortened guide below to prepare your project for this tutorial.
Prepare your Powerhouse Project to create a custom drive ### 1. Create a To-do document model: - Initialize a new project with `ph init` and give it a project name. - Start by running Vetra Studio locally with `ph vetra --watch` - Follow the [Get Started guide](/academy/GetStarted/DefineToDoListDocumentModel) to create your TodoList document model specification. - Drop the downloaded file in the Vetra Studio drive. You'll find it under document models. Vetra should now automatically generate the necessary code for your project ### 2. Add the reducer code: - Copy the code from [`todos.ts`](https://github.com/powerhouse-inc/todo-tutorial/blob/step-3-complete-implement-todo-list-document-model-reducer-operation-handlers/document-models/todo-list/src/reducers/todos.ts) - Paste it into `document-models/todo-list/src/reducers/todos.ts` ### 3. Generate a document editor: In Vetra Studio, create an Editor specification: 1. Click **"Add new specification"** in the Editors section 2. Select the **TodoList** document model 3. Name your editor `TodoList` 4. Vetra automatically generates the editor template
Alternatively: Manual generation ```bash ph generate --editor TodoList --document-types powerhouse/todo-list ```
### 4. Add the editor code: - Follow the [Build TodoList Editor guide](/academy/GetStarted/BuildToDoListEditor) to implement your editor components.
Alternatively: Use Connect instead of Vetra Studio You can also start by running Connect locally with `ph connect` instead of Vetra Studio. The workflow is the same, with Connect providing a similar development environment.
## Generate the Drive-app ### 1. Generate a Drive-app: With Vetra Studio running (`ph vetra --watch`), create a Drive-app specification: 1. In your Vetra Studio Drive, click **"Add new specification"** in the Apps section 2. Name your Drive-app `todo-list-drive-explorer` 3. Vetra automatically generates the Drive-app template in `editors/todo-list-drive-explorer/`
Alternatively: Manual generation with ph generate ```bash ph generate --drive-editor todo-list-drive-explorer ```
### 2. Update the `powerhouse.manifest.json` file: - The manifest file contains metadata for your package that is displayed when other users install it. Update the manifest to register your new Drive-app: ```json { "name": "To-do List Package", "description": "A simple todo-list with a dedicated Drive-app", "category": "Productivity", "publisher": { "name": "Powerhouse", "url": "https://www.powerhouse.inc/" }, "documentModels": [ { "id": "todo-list", "name": "TodoList" } ], "editors": [ { "id": "todo-list-editor", "name": "TodoList Editor", "documentTypes": ["todo-list"] } ], "apps": [ { "id": "todo-list-drive-explorer", "name": "TodoList drive-app", "driveEditor": "todo-list-drive-explorer" } ], "subgraphs": [], "importScripts": [] } ``` ### 3. Remove Unnecessary Default Components: - First, let's remove some default template files that we won't need for this specific demo. If you want to see what the default template looks like before removing files, you can run `ph connect` at any time. ```bash rm -rf editors/todo-list-drive-explorer/hooks rm -rf editors/todo-list-drive-explorer/components/FileItemsGrid.tsx rm -rf editors/todo-list-drive-explorer/components/FolderItemsGrid.tsx rm -rf editors/todo-list-drive-explorer/components/FolderTree.tsx ``` ### 4. Create custom components for your Drive-app: - Next, create the following files. These will define the data types for our todo-list items and provide the custom React components for our Drive-app.
Create `editors/todo-list-drive-explorer/types/todo.ts` This file defines the TypeScript type `ToDoState`. It specifies the shape of todo-list document data within the Drive-app, combining the document's revision information with its global state. This ensures that our components work with a predictable and strongly-typed data structure. ```typescript import type { TodoListDocument } from "todo-tutorial/document-models/todo-list"; export type TodoState = { documentType: string; revision: { global: number; local: number; }; global: TodoListDocument["state"]["global"]; }; ```
Create `editors/todo-list-drive-explorer/components/ProgressBar.tsx` This is a simple React component that renders a visual progress bar. It takes a `value` and `max` number to calculate the percentage of completed tasks. It also displays the percentage and has a special state for when there are no tasks. ```tsx import type { FC } from 'react'; interface ProgressBarProps { value: number; max: number; } export const ProgressBar: FC = ({ value, max }) => { if (max === 0) { return (
No tasks
); } const percentage = Math.min(100, (value / max) * 100); return (
{Math.round(percentage)}%
); }; ```
Update `editors/todo-list-drive-explorer/components/DriveExplorer.tsx` This is the main component of our Drive-app. It fetches all `powerhouse/todo-list` documents from the drive, displays them in a table with their progress, and allows a user to click on a document to open it in the `EditorContainer`. It also includes a button to create new documents. ```typescript type TodoState = { documentType: string; revision: { global: number; local: number; }; global: TodoListDocument["state"]["global"]; }; interface DriveExplorerProps { driveId: string; nodes: Node[]; onAddFolder: (name: string, parentFolder?: string) => void; onDeleteNode: (nodeId: string) => void; renameNode: (nodeId: string, name: string) => void; onCopyNode: (nodeId: string, targetName: string, parentId?: string) => void; context: DriveEditorContext; } export function DriveExplorer({ driveId, nodes, context, }: DriveExplorerProps) { const { getDocumentRevision } = context; const [activeDocumentId, setActiveDocumentId] = useState< string | undefined >(); const [openModal, setOpenModal] = useState(false); const selectedDocumentModel = useRef(null); const { addDocument, documentModels, useDriveDocumentStates } = useDriveContext(); const [state, fetchDocuments] = useDriveDocumentStates({ driveId }); useEffect(() => { fetchDocuments(driveId).catch(console.error); }, [activeDocumentId]); const { todoNodes } = useMemo(() => { return Object.keys(state).reduce( (acc, curr) => { const document = state[curr]; if (document.documentType === "powerhouse/todo-list") { acc.todoNodes[curr] = document as TodoState; } return acc; }, { todoNodes: {} as Record, }, ); }, [state]); const handleEditorClose = useCallback(() => { setActiveDocumentId(undefined); }, []); const onCreateDocument = useCallback( async (fileName: string) => { setOpenModal(false); const documentModel = selectedDocumentModel.current; if (!documentModel) return; const node = await addDocument( driveId, fileName, documentModel.documentModel.id, ); selectedDocumentModel.current = null; setActiveDocumentId(node.id); }, [addDocument, driveId], ); const onSelectDocumentModel = useCallback( (documentModel: DocumentModelModule) => { selectedDocumentModel.current = documentModel; setOpenModal(true); }, [], ); const onGetDocumentRevision = useCallback( (options?: GetDocumentOptions) => { if (!activeDocumentId) return; return getDocumentRevision?.(activeDocumentId, options); }, [getDocumentRevision, activeDocumentId], ); const filteredDocumentModels = documentModels; const fileNodes = nodes.filter((node) => node.kind === "file") as FileNode[]; // Get the active document info from nodes const activeDocument = activeDocumentId ? fileNodes.find((file) => file.id === activeDocumentId) : undefined; const documentModelModule = activeDocument ? context.getDocumentModelModule(activeDocument.documentType) : null; const editorModule = activeDocument ? context.getEditor(activeDocument.documentType) : null; return (
{/* Main Content */}
{activeDocument && documentModelModule && editorModule ? ( ) : ( <>

ToDos:

{Object.entries(todoNodes).map(([documentId, todoNode]) => ( ))}
Document ID Document Type Tasks Completed Progress
setActiveDocumentId(documentId)} className="text-blue-600 hover:text-blue-800 cursor-pointer" > {documentId}
{todoNode.documentType} {todoNode.global.stats.total} {todoNode.global.stats.checked}
{/* Create Document Section */} )}
{/* Create Document Modal */} setOpenModal(open)} open={openModal} />
); } ```
Update `editors/todo-list-drive-explorer/components/EditorContainer.tsx` This component acts as a wrapper for the document editor. When a user selects a document in `DriveExplorer.tsx`, this component mounts the appropriate editor (`todo-list-editor` in this case) and provides it with the necessary context and properties to function. It also renders the `DocumentToolbar` which provides actions like closing, exporting, and viewing revision history. ```typescript useDriveContext, type User, type DriveEditorContext, } from "@powerhousedao/reactor-browser"; documentModelDocumentModelModule, type DocumentModelModule, type EditorContext, type EditorProps, type PHDocument, type EditorModule, type Operation, } from "document-model"; DocumentToolbar, RevisionHistory, DefaultEditorLoader, generateLargeTimeline, type TimelineItem, } from "@powerhousedao/design-system"; export interface EditorContainerProps { driveId: string; documentId: string; documentType: string; onClose: () => void; title: string; context: Omit & Pick; documentModelModule: DocumentModelModule; editorModule: EditorModule; } export const EditorContainer: React.FC = (props) => { const { driveId, documentId, documentType, onClose, title, context, documentModelModule, editorModule } = props; const [selectedTimelineItem, setSelectedTimelineItem] = useState(null); const [showRevisionHistory, setShowRevisionHistory] = useState(false); const { useDocumentEditorProps } = useDriveContext(); const user = context.user as User | undefined; const timelineItems = useTimelineItems(documentId); const { dispatch, error, document } = useDocumentEditorProps({ documentId, documentType, driveId, documentModelModule, user, }); const loadingContent = (
); if (!document) return loadingContent; const moduleWithComponent = editorModule as EditorModule; const EditorComponent = moduleWithComponent.Component; return showRevisionHistory ? ( setShowRevisionHistory(false)} /> ) : ( {}} /> ); }; ```
- In case you are getting stuck and want to verify your progress with the reference repository you can find the example repository of the [Todo Tutorial here](https://github.com/powerhouse-inc/todo-tutorial) ### 3. Run the application: - With the code for our Drive-app in place, it's time to see it in action. If you've been running Vetra Studio with `ph vetra --watch`, your Drive-app is already available. Otherwise, run Connect: ```bash ph connect ``` ### Now it's your turn! Start building your own Drive-apps or experiences. Congratulations on completing this tutorial! You've successfully built a custom drive explorer, enhancing the way users interact with document models. Now, take a moment to think about the possibilities! - What **unique Drive Experiences** could you create for your own projects? - How can you tailor interfaces and streamline workflows to unlock the full potential of your document models? The Powerhouse platform provides the tools. It's time to start building! --- ## CSS Customization for Connect Integration > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/CSSCustomization When your editor runs inside Connect, it's rendered within a specific container hierarchy. Understanding this structure allows you to customize your editor's appearance to match your application's design requirements. ## Understanding the Editor Container Hierarchy Connect wraps your editor component in two key containers that you can target for styling: ```
โ””โ”€โ”€ โ””โ”€โ”€
โ””โ”€โ”€ ``` ### Container Details | Container ID | Default Classes | Data Attributes | Purpose | | ---------------------------- | ----------------- | ----------------------------------- | ----------------------------------------------------------- | | `#document-editor-container` | `flex-1` | `data-document-type` | Outermost wrapper, controls overall editor space allocation | | `#document-editor-context` | `relative h-full` | `data-editor`, `data-document-type` | Inner context, provides positioning context and full height | These containers are defined in Connect's source: - [`document-editor-container.tsx`](https://github.com/powerhouse-inc/ph-monorepo/blob/main/apps/connect/src/components/document-editor-container.tsx) (line 94) - [`editors.tsx`](https://github.com/powerhouse-inc/ph-monorepo/blob/main/apps/connect/src/components/editors.tsx) (line 173) ## Customizing Your Editor's Appearance ### Method 1: Inline Styles in Your Editor Component (Recommended) The simplest and most maintainable approach is to apply styles directly to your editor's root element. This keeps your styling self-contained within your editor. ```tsx export function Editor() { return (
{/* Your editor content */}
); } ``` **TIP:** Using `height: "100%"` ensures your editor fills the available vertical space within Connect's container hierarchy. ### Method 2: CSS File with Container Selectors For more complex customizations or when you need to override Connect's default container styles, you can target the container IDs directly in a CSS file: **DANGER:** Targeting container IDs directly will apply styles to **ALL** editors in your Connect application. For editor-specific styling, use [Method 3: Scoped Styling with Data Attributes](#method-3-scoped-styling-with-data-attributes) instead. ```css /* editors/my-editor/editor.css */ #document-editor-container { /* Customize the outer container */ background-color: #f8fafc; } #document-editor-context { /* Customize the inner context */ max-width: 1200px; margin: 0 auto; } /* Scope styles to your editor within the context */ #document-editor-context .my-editor-root { padding: 2rem; } ``` **WARNING:** Remember to import styles in your `styles.css` file rather than directly in `.tsx` files. Direct imports work in development but won't be included in production builds. ```css /* styles.css */ @import "./editors/my-editor/editor.css"; ``` ### Method 3: Scoped Styling with Data Attributes Connect adds `data-editor` and `data-document-type` attributes to editor containers, allowing you to scope CSS rules to specific editors without affecting others. ```css /* Only applies to a specific editor */ #document-editor-context[data-editor="document-editor-editor"] { background-color: #f0f9ff; } /* Only applies to a specific document type */ #document-editor-context[data-document-type="powerhouse/document-editor"] { max-width: 900px; margin: 0 auto; } /* Combine both for precise targeting */ #document-editor-context[data-editor="document-editor-editor"][data-document-type="powerhouse/document-editor"] { padding: 1rem; } ``` #### Finding Your Editor ID The `data-editor` value comes from the `id` property in your editor module configuration: ```typescript // editors/my-editor/module.ts export const MyEditor: EditorModule = { config: { id: "my-custom-editor", // <-- This becomes the data-editor value documentTypes: ["my-org/my-document-model"], }, Component: MyEditorComponent, }; ``` #### Common Editor IDs | Editor ID | Document Type | Description | | ------------------------ | ---------------------------- | --------------- | | `document-editor-editor` | `powerhouse/document-editor` | Document Editor | | `vetra-drive-app` | `powerhouse/document-drive` | Drive Explorer | | `app-editor` | `powerhouse/app` | App Editor | **TIP:** You can inspect the `data-editor` and `data-document-type` attributes in your browser's developer tools when editing a document to find the exact values for your target editor. ## Reference Implementation: Vetra Drive App The Vetra Drive App provides a real-world example of CSS customization. Here's how it styles its editor wrapper: ```tsx // From: packages/vetra/editors/vetra-drive-app/editor.tsx
``` This example demonstrates: - **`height: "100%"`** - Ensures the editor fills the full container height - **`bg-gray-50`** - Applies a light gray background color - **`p-6`** - Adds consistent padding around the content - **`after:*` pseudo-element** - Creates a visual effect layer for transitions ## Common Use Cases ### Full-Height Editor with Scrollable Content When your editor needs a fixed header/toolbar with scrollable main content: ```tsx export function Editor() { return (

Document Title

{/* Toolbar buttons */}
{/* Scrollable content area */}
); } ``` ### Custom Background and Theming Apply custom backgrounds or gradients to match your application's theme: ```tsx export function Editor() { return (
{/* Themed content */}
); } ``` ### Centered Content with Max Width Constrain content width for better readability: ```tsx export function Editor() { return (
{/* Centered, width-constrained content */}
); } ``` ## Troubleshooting ### Editor Doesn't Fill Available Height **Problem:** Your editor content appears squished or doesn't use the full height. **Solution:** Ensure your root element has `height: "100%"` or uses flex utilities: ```tsx // Option 1: Inline style
// Option 2: Tailwind class
``` ### Styles Not Applied in Production **Problem:** Styles work in development but not in production builds. **Solution:** Move style imports from `.tsx` files to your `styles.css` file: ```css /* styles.css - correct location for imports */ @import "./editors/my-editor/editor.css"; ``` ### Z-Index Conflicts **Problem:** Overlays, modals, or dropdowns appear behind other elements. **Solution:** The `#document-editor-context` has `position: relative`. Use this as your positioning context: ```tsx
{/* Your content */}
{/* Positioned overlay */}
``` ### Content Overflows Container **Problem:** Content extends beyond the editor boundaries. **Solution:** Add overflow handling to your root element: ```tsx
{/* Scrollable when content overflows */}
// Or hide overflow
{/* Content is clipped */}
``` ## Further Reading - [Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) - Fundamentals of editor development including basic styling - [Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) - Creating custom drive apps with styling --- ## Document Toolbar > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/DocumentToolbar **WARNING:** This documentation is still being written and may be incomplete. The Document Toolbar is a central component in Powerhouse Connect, appearing at the top of every document view. It provides quick access to a variety of tools and functions designed to streamline your workflow and enhance document management.
Document Toolbar
The Document Toolbar can be found at the top of any generic document.
## Overview The toolbar is designed to be intuitive and context-aware, offering relevant options based on the document type and your current task. Its capabilities range from basic actions to more advanced document control features. ### Key functionalities in the future will include: - **Switchboard API Access:** Access the Switchboard API via its logo in the toolbar. This opens the Apollo Studio Sandbox, pre-populating the DocumentID for your current document model. This feature is unavailable for locally stored documents. - **Navigation and Versioning:** Tools for managing [document versions](/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/OperationHistory), such as undo/redo, and accessing the [revision history timeline](/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/RevisionHistoryTimeline). - **Document Actions:** Buttons for common operations like exporting or deleting documents. - **Information Display:** Shows the document title and may include indicators for status (e.g., connectivity, errors) or active modes. - **Search and Filtering:** Integrated search capabilities to find assets or attachments within the document context. - **View Customization:** Options to switch between different views or display modes. - **Contextual Tools:** Depending on the document, you might find toggles, dropdowns for specific actions (e.g., "Sync Schema", "Fixed Income Purchase"), or buttons for settings and configurations. The specific set of tools available on the toolbar can be configured and may evolve as new features are introduced. This section will detail the various components and options you may encounter. _(Detailed information on each toolbar feature, such as "Export Button", "Title Display", "Search Functionality", "Toggle Switches", "Icon Buttons", etc., will follow here.)_ --- ## Operations history > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/OperationHistory ## What is a document model? A **document model** in Powerhouse is the core unit for managing business data. Each document (such as an invoice, contributor agreement, or scope of work) is created from a specific document model, which defines: - **State schema:** What data the document contains - **Operations:** What actions can modify that document ## What is an operations history? Every time a user edits a document, they do so by submitting a document operation or a 'command' in CQRS (e.g., `ADD_LINE_ITEM`, `UPDATE_RECIPIENT`). These operations are: - **Appended** to the document's history - **Immutable** (you never overwrite; you always append) - **Replayable** โ€” the current document state is the result of applying all past operations in sequence This design is based on **event sourcing** principles. ## Why use an operations history? - **Auditability:** Inspect every change ever made to a document. - **Transparency:** Contributors see what others have done. - **Versioning:** Revert to any prior state or resolve conflicts using branching and merging. - **Interoperability:** Operations are just dataโ€”they can be signed, stored on-chain, or synchronized across systems. ## Example: Invoice document Suppose you're editing a `powerhouse/invoice` document. Its operations history might look like this: ```plaintext SET_RECIPIENT(name: "Acme Corp") ADD_LINE_ITEM(description: "Design Work", unitCost: $500) UPDATE_DUE_DATE(date: "2025-06-01") ``` The document's state at any time is the result of running those operations in order. ## Visualizing the operations history ### Revision list and details In Connect the Powerhouse UI displays a chronologic list of all applied modifications to the document, each with a unique event ID, state hash, and commit message. You can inspect each revision for signatures and errors. ![Revision History List](../docs/docs/images/revision-history-list.png) ### Viewing revision hashes and event IDs Hovering over a revision reveals its event ID and state hash, providing traceability for every change. ![Revision Hash Popup](../docs/docs/images/revision-hash-popup.png) ### Signature verification Clicking the signature badge shows signature details, including signer address, hash, and verification status. This ensures every operation is cryptographically auditable. Read more about how we are using [Renown](/academy/MasteryTrack/BuildingUserExperiences/Authorization/RenownAuthenticationFlow) for authentication & verification of signer data. ![Signature Details Popup](../docs/docs/images/signature-details-popup.png) ### Viewing committer addresses You can also view the committer's address for each revision, supporting full transparency and accountability. ![Committer Address Popup](../docs/docs/images/committer-address-popup.png) **WARNING:** This remainder of this documentation is still being written and may be incomplete. ## Replay, branch, and merge (under development) - **Replay:** When you load a document, the system replays all operations to build its state. - **Branch:** Create a parallel version of the document to test changes or handle conflicts. - **Merge:** Combine branches intelligently based on operations, not just raw field values. --- ## Revision history timeline > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/RevisionHistoryTimeline The history timeline feature enables users to view document history and navigate through different revisions of a document. This guide explains how to implement and customize the timeline functionality in your Powerhouse application. ## How to enable the timeline feature To enable the timeline feature in your document editor, you need to set `timelineEnabled: true` in your editor module configuration: ```typescript // editors/to-do-list/index.ts export const module: EditorModule = { Component: Editor as unknown as FC< EditorProps & Record >, documentTypes: ["powerhouse/todo-list"], }; ``` This setting enables the timeline button in the document toolbar. **WARNING:** The revision history timeline will only become visible once your document model has some operations or 'history'. Add a few to-do's or some data in the model you are working on and the revision history timeline button in the Document Toolbar will be activated. Click the button to see the timeline expand and see the first history 'candle' appear.
revision history timeline
Once your document has a few operations added to it's history the revision history timeline gets activated.
## How to implement the timeline feature ### Default Drive-app When using the default Drive-app with `ph connect`, the timeline functionality is handled automatically: - Document analytics are collected and passed to the document toolbar - The timeline button appears in the toolbar when enabled - Users can click on timeline items to view document revisions ### Custom Drive-app For custom Drive-apps, you need to handle timeline items fetching and user interaction manually. Here's how: 1. First, import the necessary utilities from the Powerhouse common package: ```typescript ``` 2. Fetch timeline items using the `useTimelineItems` hook: ```typescript // In your EditorContainer.tsx const timelineItems = useTimelineItems(documentId); ``` 3. Track the selected timeline item in state: ```typescript const [selectedTimelineItem, setSelectedTimelineItem] = useState(null); ``` 4. Pass the timeline items to the DocumentToolbar and handle item selection: ```typescript ``` 5. Pass the required context values to your editor component: ```typescript ``` ## Handling timeline revisions in document editor In your document editor (e.g., `editors/to-do-list/editor.tsx`), you need to handle the timeline context props: 1. Extract timeline-related properties from the context: ```typescript const { readMode = false, selectedTimelineRevision, getDocumentRevision, } = context; ``` 2. Fetch the document at the selected revision when in read mode: ```typescript const [readModeDocument, setReadModeDocument] = useState( null, ); useEffect(() => { const getReadModeDocument = async () => { if ( getDocumentRevision && readMode && typeof selectedTimelineRevision === "number" ) { const document = await getDocumentRevision({ revisions: { global: selectedTimelineRevision }, }); setReadModeDocument(document); } else if (!readMode) { setReadModeDocument(null); } }; getReadModeDocument(); }, [getDocumentRevision, readMode, selectedTimelineRevision]); // Use the appropriate document based on mode const document = readModeDocument || writeModeDocument; ``` 3. Adapt your UI to reflect read mode: ```typescript {readMode && (
(๐Ÿ”’ Read Mode)
)} {!readMode && ( // Edit controls here )} ``` This implementation allows users to navigate through document history while preventing edits to historical revisions. --- ## Inspector Modal > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/InspectorModal The Inspector Modal is a development and debugging tool in Connect that provides visibility into the internal state of your application. It allows you to inspect the local database and monitor sync operations with remote servers. The Inspector Modal has two tabs: - **Database** - Explore tables and data in the local PGlite database - **Remotes** - View sync remotes and their channel states (inbox, outbox, dead letter) ## Accessing the Inspector Modal The Inspector is available to every Connect user through the Settings menu: 1. Click the **Settings** (โš™๏ธ) button in the Connect sidebar footer 2. Select the **About** tab 3. Click the **Open Inspector** button 4. The Inspector Modal opens with two tabs: **Database** and **Remotes** ## Database Explorer The Database tab allows you to inspect the local PGlite database that Connect uses to store documents and application data.
Database Explorer interface
The Database Explorer showing the schema tree and table data view.
### Understanding the Interface The Database Explorer is divided into two panels: - **Left panel (Schema Tree)**: Shows all tables in the `public` schema with their column information - **Right panel (Table View)**: Displays the data for the selected table with pagination and sorting ### Browsing Tables 1. In the left sidebar, you'll see the schema tree with all available tables 2. Click on a table name to expand it and view its columns 3. Each column shows: - Column name - Data type (e.g., `varchar`, `boolean`, `integer`) - Whether the column is nullable
Schema tree with expanded table
The schema tree showing tables and their column details.
### Viewing Table Data 1. Select a table from the schema tree to load its data in the right panel 2. The table view displays all rows with pagination controls at the bottom 3. Use the pagination controls to navigate through large datasets: - First page / Previous page - Page numbers - Next page / Last page 4. Click any column header to sort the data: - Click once for ascending order - Click again for descending order
Table view with pagination
The table data view with pagination controls.
### Exporting the Database To create a backup of your local database: 1. Click the **Export DB** button in the sidebar 2. A complete SQL dump file will be downloaded to your computer 3. The file is named `database-export-{timestamp}.sql` This export contains the full database schema and all data, which can be useful for debugging or creating backups. ### Importing a Database **WARNING:** Importing a database will **replace ALL existing data** in your local Connect instance. This action cannot be undone. To import a database: 1. Click the **Import DB** button in the sidebar 2. Select a `.sql` or `.txt` file containing valid SQL 3. Confirm the replacement in the dialog that appears 4. The database will be cleared and replaced with the imported data 5. The schema tree will refresh automatically to show the new tables ## Remotes Explorer The Remotes tab allows you to inspect sync remotes and their channel states. This is useful for debugging synchronization issues between your local Connect instance and remote servers.
Remotes Explorer interface
The Remotes Explorer showing configured sync remotes.
### Understanding Remotes Sync remotes are connections between your local Connect instance and remote reactors (servers). When you add a remote drive or sync with a cloud service, a remote is created to manage the bidirectional synchronization of document operations. ### Viewing Remote List The Remotes tab displays a table with all configured sync remotes: | Column | Description | | ------------- | -------------------------------------------------------- | | ID | Unique identifier for the remote (truncated) | | Name | Human-readable name of the remote | | Collection ID | The collection this remote is associated with | | Filter | Configuration showing branch, document, or scope filters | | Channel | "View" button to inspect the remote's channel state | ### Inspecting Remote Channels Click the **View** button on any remote to open the Channel Inspector. The channel shows three mailboxes that track sync operations:
Channel Inspector with mailboxes
The Channel Inspector showing Inbox, Outbox, and Dead Letter mailboxes.
#### Inbox Operations **received from** the remote server: | Column | Description | | ----------- | ------------------------------------------------------ | | ID | Operation identifier | | Document ID | The document this operation affects | | Branch | The document branch (usually "main") | | Status | Current status: Pending (โณ), Applied (โœ…), Error (โŒ) | | Ops Count | Number of operations in this batch | #### Outbox Operations **waiting to be sent** to the remote server. Shows the same columns as Inbox. #### Dead Letter Operations that **failed to sync** with error information: | Column | Description | | ----------- | ------------------------------------ | | ID | Operation identifier | | Document ID | The document this operation affects | | Branch | The document branch | | Error | Error message explaining the failure | ## Use Cases The Inspector Modal is helpful for: - **Debugging sync issues**: Check the Remotes tab to see if operations are stuck in the inbox/outbox or if there are errors in the dead letter queue - **Verifying data integrity**: Browse tables to ensure documents are stored correctly - **Exporting data for backup**: Create SQL dumps of your local database - **Understanding data flow**: See how operations move between local and remote systems - **Troubleshooting**: Inspect table schemas and data when debugging application issues --- ## Renown authentication flow > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/Authorization/RenownAuthenticationFlow The Renown login flow leverages decentralized identity (DID) creation and the Ceramic network for credential storage and verification, ensuring secure and verifiable actions within decentralized organizations. Below is a detailed breakdown of the process, aimed at developers integrating the Renown, Connect, and Switchboard components. ### Renown in the decentralized workplace Renown provides a decentralized identity and reputation hub, where users connect their Ethereum address, creating a **Decentralized Identifier (DID)** tied to their wallet. #### Why an integrated identity solution? Renown is designed to address the challenge of trust within DAOs, where contributors often operate under pseudonyms. In traditional organizations, personal identity and reputation are key to establishing trust and accountability. Renown replicates this dynamic in the digital space, allowing contributors to earn experience and build reputation without revealing their real-world identities. Over time, contributors can share their pseudonymous profiles with other organizations as cryptographic resumes, helping to secure new opportunities while maintaining privacy. ### Detailed login flow #### 1. User login via wallet connection - The user starts by logging into Renown using their Ethereum address. This is done by signing a message with their wallet. - The Ethereum key is used to create a DID (Decentralized Identifier), which uniquely represents the user without exposing their personal identity.
Connect Address
Connecting your Ethereum address to generate Decentralized Identifier with Renown.
#### 2. DID creation - A Decentralized Identifier (DID) is created based on the user's Ethereum key. The DID follows a specific format: `did:example:123456789abcdefghijk` - This DID acts as the user's digital identifier across decentralized systems. #### 3. Credential generation - A credential is generated, allowing the DID to sign operations on behalf of the user. This credential is stored on a Powerhouse-hosted identity node. - The identity node ensures that the credentials are securely stored and verifiable across the network. Credentials include the user's signing permissions and are linked to the DID. - Powerhouse aims to decentralize this identity reactor over time while maintaining an efficient hub for using your decentralized identity and reputation to explore different organizations. #### 4. Operation signing with Connect - Connect uses the newly created DID to sign operations performed by the user. For example, when a document or transaction is edited in Connect, the operation is signed with the user's DID. - This ensures that every action taken within the Connect system is linked to the user's decentralized identity, maintaining transparency and accountability.
Renown Login
Your DID is used to sign operations performed by the user.
#### 5. Switchboard verification - Once an operation is signed by the DID through Connect, it is sent to Switchboard for verification. - Switchboard verifies whether the DID has a valid credential stored on the Powerhouse identity node and if the DID was indeed the one that signed the operation. - The request includes critical metadata such as the user's Ethereum address, the DID, the signed operation, and other parameters required for validation. ```json { "signerAddress": "0x1234...", "hash": "did:key:2be4x...", "signatureBytes": "Xmqy8FNz...", "isVerified": true } ``` #### 6. Operation validation and execution - After Switchboard validates the operation, it ensures the operation context is accurate and the credentials match the signer. - The operation is then either approved or rejected based on the verification results. - Approved operations are processed, and changes made within the Connect system are synchronized across the relevant nodes. **INFO:** **Key Components used during login-flow** - **Renown**: Manages user identities via DID creation and Ethereum wallet integration. - **Powerhouse Identity Node**: Hosts user credentials and enables verification. Powerhouse is working towards decentralizing this identity reactor over time. - **Connect**: The interface for users to perform operations. It uses the DID for signing operations. - **Switchboard**: Responsible for verifying credentials and operation signatures to ensure authenticity. ::: This flow ensures that all actions within the Powerhouse ecosystem are secure, transparent, and verifiable, promoting trust in a pseudonymous contributor environment. --- ## Document Permission System > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/Authorization/DocumentPermissions **WARNING:** This documentation is still being written and may be incomplete. The feature is not yet available on production. ## Introduction The **Reactor API** is the API interface to a Powerhouse **Reactor**โ€”a storage node responsible for storing documents, resolving conflicts, and verifying document event histories. Reactors can be configured for local storage, cloud storage, or decentralized storage networks. Within a Reactor, data is organized hierarchically: - **Drives**: Logical containers for organizing collections of documents (similar to a repository) - **Folders**: Subdirectories within drives for organizing documents - **Documents**: Individual Powerhouse documents based on document models (e.g., todo lists, budgets, specifications) ### Why Document Permissions? The Reactor API implements a two-layer authorization system: | Component | System | Purpose | Scope | | ----------------------- | ---------------------------------------------------------- | ---------------------------------------------------- | ------------ | | **Auth & Admin** | [**Authentication & Admin Access**](../docs/docs/04-Authorization.md) | Controls authentication and supreme admin access | Reactor-wide | | **Protection & Grants** | **Document Permission System** (this guide) | Controls access to specific documents/folders/drives | Per-document | This document permission system allows you to implement fine-grained access controlโ€”for example, you might want certain team members to have write access to a "Marketing" drive while only having read access to a "Finance" drive, even though they're both authenticated users of your Reactor. **INFO:** Before using document permissions, you must configure [authentication](../docs/docs/04-Authorization.md) with `AUTH_ENABLED=true`. Users need a valid authentication token to access the Reactor API. Supreme admins (listed in `ADMINS`) bypass all permission checks. ## Overview The document permission system provides: - **Document-level permissions**: Grant READ, WRITE, or ADMIN access to specific documents - **Permission inheritance**: Permissions flow down from parent documents (drives โ†’ folders โ†’ documents) - **Group-based access**: Organize users into groups and assign permissions to groups - **Operation-level permissions**: Restrict specific operations to certain users or groups ## Prerequisites Before using document permissions, you need: 1. **A running Reactor**: Start a local Reactor instance 2. **Renown authentication**: Users must authenticate via `ph login` via the CLI 3. **AUTH_ENABLED=true**: Global authentication must be enabled 4. **DOCUMENT_PERMISSIONS_ENABLED=true**: Document permissions feature flag must be set ### Starting the Reactor API The Reactor API is started when you run a local Reactor instance. You have several options: **Option 1: Run Reactor directly** ```bash # Start a local Reactor on port 4001 (default) ph reactor # Or specify a custom port ph reactor --port 5000 ``` **Option 2: Run both Connect and Reactor together** ```bash # In one terminal: start the Reactor ph reactor # In another terminal: start Connect Studio ph connect ``` All options will start a Reactor API instance, typically accessible at: - **GraphQL API**: `http://localhost:4001/graphql` - **Drive endpoint**: `http://localhost:4001/d/powerhouse` ### Accessing Remote/Production Reactors For production or remote Reactor instances (e.g., hosted on Vetra): - The Reactor is already running on a remote server - Access it via the remote URL (e.g., `https://vetra.example.com/d/your-drive-id`) - You still need to authenticate with `ph login` and obtain an access token - Include your bearer token in API requests to the remote Reactor's GraphQL endpoint via an header of the type 'Authorization' ### Configuring Authentication Once the Reactor is running, enable authentication in your `powerhouse.config.json` or via environment variables: ```bash # Environment variables AUTH_ENABLED=true DOCUMENT_PERMISSIONS_ENABLED=true ``` Or in `powerhouse.config.json`: ```json { "auth": { "enabled": true } } ``` After configuration changes, restart the Reactor for settings to take effect. ## Quick Start Here's the complete flow to set up and use document permissions: 1. **Start the Reactor with auth enabled**: ```bash # Set environment variables export AUTH_ENABLED=true export DOCUMENT_PERMISSIONS_ENABLED=true # Start the Reactor ph reactor ``` 2. **Authenticate as a user**: ```bash # Login via Renown ph login # Generate an access token ph access-token --expiry 7d ``` 3. **Grant yourself admin access to a drive** (via GraphQL or programmatically when creating the drive) 4. **Grant permissions to other users or groups** using the GraphQL mutations described below ## Permission Levels The system defines three permission levels for documents: | Level | Description | Capabilities | | ------- | ----------- | -------------------------------------------- | | `READ` | View access | Can fetch and read the document | | `WRITE` | Edit access | Can push updates and modify the document | | `ADMIN` | Full access | Can manage document permissions and settings | **Permission hierarchy**: Higher permission levels include all capabilities of lower levels (ADMIN includes WRITE and READ, WRITE includes READ). **TIP:** - **READ**: A team member can view a budget document but cannot make changes - **WRITE**: A contributor can edit a specification document and push updates - **ADMIN**: A project lead can modify who has access to the entire project drive ## How It Works ### Permission Resolution When a user attempts to access a document, the Reactor API checks permissions in this order: 1. **[Authentication check](../docs/docs/04-Authorization.md)**: First, verify the user is authenticated and check if they are a supreme admin (`ADMINS`). If `AUTH_ENABLED=false`, access is granted to everyone. Supreme admins bypass all further checks 2. **Direct user permission**: Check if the user has explicit permission on the document 3. **Group permission**: Check if the user belongs to a group with permission on the document 4. **Parent inheritance**: Recursively check parent documents (folder โ†’ drive) **Example of permission inheritance:** ``` Drive (ADMIN permission for user Alice) โ””โ”€โ”€ Folder A (inherits ADMIN from Drive) โ””โ”€โ”€ Document 1 (inherits ADMIN from Folder A) โ””โ”€โ”€ Document 2 (inherits ADMIN from Folder A) โ””โ”€โ”€ Folder B (READ permission granted explicitly to Alice) โ””โ”€โ”€ Document 3 (inherits READ from Folder B) ``` In this example: - Alice has ADMIN on the Drive, so she inherits ADMIN on Folder A and its documents - Alice was explicitly granted READ on Folder B, so Document 3 only has READ access - Without explicitly setting permissions on Folder B, all folders would inherit ADMIN from the Drive ### Database Schema The Reactor API stores permission data in a relational database using six tables: | Table | Purpose | | -------------------------- | -------------------------------------------------------------------------- | | `DocumentPermission` | Direct user-document permissions (e.g., "Alice has WRITE on Document X") | | `Group` | Group definitions (e.g., "Engineering Team") | | `UserGroup` | User-group memberships (e.g., "Alice is in Engineering Team") | | `DocumentGroupPermission` | Group-document permissions (e.g., "Engineering Team has READ on Drive Y") | | `OperationUserPermission` | User-operation permissions (e.g., "Bob can execute DELETE_NODE") | | `OperationGroupPermission` | Group-operation permissions (e.g., "Admins group can execute DELETE_NODE") | **INFO:** These tables are automatically created by database migrations when you enable `DOCUMENT_PERMISSIONS_ENABLED=true`. You don't need to create them manually.
Useful Queries #### Get document access info ```graphql query GetDocumentAccess($documentId: String!) { documentAccess(documentId: $documentId) { documentId groupPermissions { documentId groupId group { id name description members } permission grantedBy } permissions { documentId userAddress permission } } } ``` #### Get current user's document permissions ```graphql query MyDocuments { userDocumentPermissions { documentId permission grantedBy createdAt } } ``` #### List all groups ```graphql query ListGroups { groups { id name description members } } ``` #### Check operation permission ```graphql query CanExecute($documentId: String!, $operation: String!) { canExecuteOperation(documentId: $documentId, operationType: $operation) } ``` #### Get group by ID ```graphql query GetGroup($id: Int!) { group(id: $id) { id name description members createdAt updatedAt } } ``` #### Get user groups ```graphql query GetUserGroups($userAddress: String!) { userGroups(userAddress: $userAddress) { id name description members } } ``` #### Get operation permissions ```graphql query GetOperationPermissions($documentId: String!, $operationType: String!) { operationPermissions(documentId: $documentId, operationType: $operationType) { documentId operationType userPermissions { documentId operationType userAddress grantedBy createdAt } groupPermissions { documentId operationType groupId group { id name } grantedBy createdAt } } } ``` #### Document Drive Operation Types For document drives specifically, the following operation permissions are available: - `ADD_FILE` - Create new files within the drive - `ADD_FOLDER` - Create new folders within the drive - `DELETE_NODE` - Delete files or folders within the drive - `UPDATE_FILE` - Modify existing files within the drive - `UPDATE_NODE` - Update properties of files or folders within the drive (including renaming documents) - `COPY_NODE` - Copy files or folders within the drive - `MOVE_NODE` - Move files or folders within the drive **INFO:** These operation permissions provide fine-grained control over specific actions within a document drive, separate from the general document permission levels (READ, WRITE, ADMIN). Note that renaming a document is not part of the WRITE permission on the document itselfโ€”it's an `UPDATE_NODE` operation on the drive document. If you need to set operation permissions for documents with different document models, familiarize yourself with the available operations of the installed document model package.
Useful Mutations ### Document Permissions #### Grant document permission to user ```graphql mutation GrantDocumentPermission( $documentId: String! $userAddress: String! $permission: DocumentPermissionLevel! ) { grantDocumentPermission( documentId: $documentId userAddress: $userAddress permission: $permission ) { documentId userAddress permission grantedBy createdAt updatedAt } } ``` #### Revoke document permission from user ```graphql mutation RevokeDocumentPermission($documentId: String!, $userAddress: String!) { revokeDocumentPermission(documentId: $documentId, userAddress: $userAddress) } ``` ### Group Management #### Create group ```graphql mutation CreateGroup($name: String!, $description: String) { createGroup(name: $name, description: $description) { id name description createdAt updatedAt members } } ``` #### Delete group ```graphql mutation DeleteGroup($id: Int!) { deleteGroup(id: $id) } ``` #### Add user to group ```graphql mutation AddUserToGroup($userAddress: String!, $groupId: Int!) { addUserToGroup(userAddress: $userAddress, groupId: $groupId) } ``` #### Remove user from group ```graphql mutation RemoveUserFromGroup($userAddress: String!, $groupId: Int!) { removeUserFromGroup(userAddress: $userAddress, groupId: $groupId) } ``` ### Group Permissions #### Grant group permission ```graphql mutation GrantGroupPermission( $documentId: String! $groupId: Int! $permission: DocumentPermissionLevel! ) { grantGroupPermission( documentId: $documentId groupId: $groupId permission: $permission ) { documentId groupId group { id name } permission grantedBy createdAt updatedAt } } ``` #### Revoke group permission ```graphql mutation RevokeGroupPermission($documentId: String!, $groupId: Int!) { revokeGroupPermission(documentId: $documentId, groupId: $groupId) } ``` ### Operation Permissions #### Grant operation permission to user ```graphql mutation GrantOperationPermission( $documentId: String! $operationType: String! $userAddress: String! ) { grantOperationPermission( documentId: $documentId operationType: $operationType userAddress: $userAddress ) { documentId operationType userAddress grantedBy createdAt } } ``` #### Revoke operation permission from user ```graphql mutation RevokeOperationPermission( $documentId: String! $operationType: String! $userAddress: String! ) { revokeOperationPermission( documentId: $documentId operationType: $operationType userAddress: $userAddress ) } ``` #### Grant group operation permission ```graphql mutation GrantGroupOperationPermission( $documentId: String! $operationType: String! $groupId: Int! ) { grantGroupOperationPermission( documentId: $documentId operationType: $operationType groupId: $groupId ) { documentId operationType groupId group { id name } grantedBy createdAt } } ``` #### Revoke group operation permission ```graphql mutation RevokeGroupOperationPermission( $documentId: String! $operationType: String! $groupId: Int! ) { revokeGroupOperationPermission( documentId: $documentId operationType: $operationType groupId: $groupId ) } ```
Document Management #### Create document ```graphql mutation CreateDocument($document: JSONObject!, $parentIdentifier: String) { createDocument(document: $document, parentIdentifier: $parentIdentifier) { id name documentType state createdAtUtcIso lastModifiedAtUtcIso parentId } } ``` #### Create empty document ```graphql mutation CreateEmptyDocument( $documentType: String! $parentIdentifier: String ) { createEmptyDocument( documentType: $documentType parentIdentifier: $parentIdentifier ) { id name documentType state createdAtUtcIso lastModifiedAtUtcIso parentId } } ``` #### Mutate document ```graphql mutation MutateDocument( $documentIdentifier: String! $actions: [JSONObject!]! $view: ViewFilterInput ) { mutateDocument( documentIdentifier: $documentIdentifier actions: $actions view: $view ) { id name documentType state revisionsList { scope revision } lastModifiedAtUtcIso } } ``` #### Rename document ```graphql mutation RenameDocument( $documentIdentifier: String! $name: String! $branch: String ) { renameDocument( documentIdentifier: $documentIdentifier name: $name branch: $branch ) { id name documentType lastModifiedAtUtcIso parentId } } ``` #### Delete document ```graphql mutation DeleteDocument($identifier: String!, $propagate: PropagationMode) { deleteDocument(identifier: $identifier, propagate: $propagate) } ```
Drive Management #### Add drive ```graphql mutation AddDrive( $name: String! $icon: String $id: String $slug: String $preferredEditor: String ) { addDrive( name: $name icon: $icon id: $id slug: $slug preferredEditor: $preferredEditor ) { id slug name icon preferredEditor } } ``` #### Delete drive ```graphql mutation DeleteDrive($id: String!) { deleteDrive(id: $id) } ``` #### Set drive icon ```graphql mutation SetDriveIcon($id: String!, $icon: String!) { setDriveIcon(id: $id, icon: $icon) } ``` #### Set drive name ```graphql mutation SetDriveName($id: String!, $name: String!) { setDriveName(id: $id, name: $name) } ```
## GraphQL API The Reactor API exposes a GraphQL interface for all operations. When `DOCUMENT_PERMISSIONS_ENABLED=true`, an **Auth subgraph** is registered that adds permission-related queries and mutations to the API. **TIP:** All GraphQL operations below require: 1. A valid bearer token from `ph access-token` 2. The token included in the `Authorization` header followed by 'Bearer ``' 3. The Reactor API running with both `AUTH_ENABLED=true` and `DOCUMENT_PERMISSIONS_ENABLED=true` as variables ## Configuration You can configure the Reactor API using either environment variables or a `powerhouse.config.json` file. Environment variables take precedence over the config file. ### Environment Variables ```bash # Enable authentication (required for document permissions) AUTH_ENABLED=true # Enable document permissions feature (requires AUTH_ENABLED=true) DOCUMENT_PERMISSIONS_ENABLED=true # Supreme admin addresses (bypass all permission checks) # These users are Reactor-wide administrators ADMINS="0x123...,0x456..." # Make all new documents protected by default (requires explicit grants for access) DEFAULT_PROTECTION=true ``` ### powerhouse.config.json Alternatively, configure authorization in your `powerhouse.config.json` file: ```json { "auth": { "enabled": true, "admins": ["0x123...", "0x456..."] } } ``` **INFO:** - **[Authentication & Admin Access](../docs/docs/04-Authorization.md)** (`AUTH_ENABLED`, `ADMINS`): Controls authentication and supreme admin bypass - **Document protection**: Determines whether a document requires explicit grants (configurable via `DEFAULT_PROTECTION` or per-document) - **Document permissions** (this guide): Manages fine-grained READ/WRITE/ADMIN grants on specific documents Users must be authenticated before document permissions are evaluated. Supreme admins (`ADMINS`) bypass all permission checks. ## Usage Examples: Company Document Access & Permissions This section provides practical examples for common permission management scenarios. These examples assume you have: - A running Reactor API with `AUTH_ENABLED=true` and `DOCUMENT_PERMISSIONS_ENABLED=true` - A valid bearer token from `ph access-token` - The Ethereum address of users you want to grant permissions to
Complete Scenario: Company Document Access & Permissions This walkthrough demonstrates setting up document access for a company with different departments and roles. We'll create a "Finance Team" group, add team members, create a confidential finance drive, and configure granular permissions for sensitive financial documents. This scenario demonstrates advanced authorization patterns for managing contributor access levels to a shared finances todo list document, focusing on role-based access control and operation-level permissions using the @powerhousedao/todo-demo package. To get access to this subgraph yourself make sure to install the package in your project with `ph install @powerhousedao/todo-demo`
Todo Document Schema Reference ```graphql type TodoList implements IDocument { id: String! name: String! documentType: String! operations(skip: Int, first: Int): [Operation!]! revision: Int! createdAtUtcIso: DateTime! lastModifiedAtUtcIso: DateTime! initialState: TodoList_TodoListState! state: TodoList_TodoListState! stateJSON: JSONObject } """ Module: Todos """ input TodoList_AddTodoItemInput { text: String! } input TodoList_DeleteTodoItemInput { id: OID! } type TodoList_TodoItem { id: OID! text: String! checked: Boolean! } type TodoList_TodoListState { items: [TodoList_TodoItem!]! } input TodoList_UpdateTodoItemInput { id: OID! text: String checked: Boolean } """ Queries: TodoList Document """ type TodoListQueries { getDocument(docId: PHID!, driveId: PHID): TodoList getDocuments(driveId: String!): [TodoList!] } ```
### Step 1: Start the Reactor with Authentication We have 3 options to configure authorization rules: - Environment variables - `powerhouse.config.json` file - CLI command during local development For this example, we'll use the CLI approach: ```bash AUTH_ENABLED=true DOCUMENT_PERMISSIONS_ENABLED=true ADMINS=0x1234...abcd ph switchboard ``` **Expected CLI output:** ``` [switchboard] [connect-crypto] Switchboard identity initialized: did:key:zDnaepECmgs6RCDdjVo2RTYyPxoW5ZC4hN4iHrHH9Su9cXdM1 [reactor-api] [server] Setting up Auth middleware [reactor-api] [server] Document permission migrations completed [reactor-api] [server] Document permission service initialized ``` โœ… Authorization and document permissions have been initialized successfully. ### Step 2: Authenticate and Get Access Token Now authenticate and generate a bearer token for API requests: ```bash # Authenticate with your admin wallet ph login # Generate access token (valid for 7 days) ph access-token --expiry 7d ``` Copy the access token and include it in your GraphQL requests as: `Authorization: Bearer ` ### Step 3: Create the Finance Team Group **Mutation:** ```graphql mutation CreateGroup($name: String!) { createGroup(name: $name) { name id } } ``` **Variables:** ```json { "name": "finance-team" } ``` **Expected Result:** ```json { "data": { "createGroup": { "name": "finance-team", "id": 1 } } } ``` ### Step 4: Add Finance Team Members **Add Finance Manager (Alice):** **Mutation:** ```graphql mutation AddUserToGroup($userAddress: String!, $groupId: Int!) { addUserToGroup(userAddress: $userAddress, groupId: $groupId) } ``` **Variables:** ```json { "userAddress": "0xalice...finance", "groupId": 1 } ``` **Expected Result:** ```json { "data": { "addUserToGroup": true } } ``` **Add Financial Analyst (Bob):** Use the same mutation with different variables: **Variables:** ```json { "userAddress": "0xbob...analyst", "groupId": 1 } ``` ### Step 5: Create a Finance Drive **Mutation:** ```graphql mutation AddDrive($name: String!, $addDriveId: String, $slug: String) { addDrive(name: $name, id: $addDriveId, slug: $slug) { id name slug } } ``` **Variables:** ```json { "name": "finance-documents", "addDriveId": null, "slug": null } ``` **Expected Result:** ```json { "data": { "addDrive": { "id": "drive-uuid-1234-5678-abcd", "name": "finance-documents", "slug": "finance-documents" } } } ``` ๐Ÿ“ **Note:** Copy the returned `id` for the next steps. ### Step 6: Grant Finance Team Write Access to the Drive **Mutation:** ```graphql mutation GrantGroupPermission( $documentId: String! $groupId: Int! $permission: DocumentPermissionLevel! ) { grantGroupPermission( documentId: $documentId groupId: $groupId permission: $permission ) { groupId grantedBy documentId group { name } } } ``` **Variables:** ```json { "documentId": "drive-uuid-1234-5678-abcd", "groupId": 1, "permission": "WRITE" } ``` **Expected Result:** ```json { "data": { "grantGroupPermission": { "groupId": 1, "grantedBy": "0x1234...abcd", "documentId": "drive-uuid-1234-5678-abcd", "group": { "name": "finance-team" } } } } ``` ### Step 7: Grant Specific Operation Permission Give Finance Manager permission to execute the `ADD_FILE` operation for creating new financial documents. See the [Document Drive Operation Types](#document-drive-operation-types) section for all available operation permissions: **Mutation:** ```graphql mutation GrantOperationPermission( $documentId: String! $operationType: String! $userAddress: String! ) { grantOperationPermission( documentId: $documentId operationType: $operationType userAddress: $userAddress ) { operationType userAddress documentId } } ``` **Variables:** ```json { "documentId": "drive-uuid-1234-5678-abcd", "operationType": "ADD_FILE", "userAddress": "0xalice...finance" } ``` **Expected Result:** ```json { "data": { "grantOperationPermission": { "operationType": "ADD_FILE", "userAddress": "0xalice...finance", "documentId": "2d707e84-309a-4b69-803a-400786806ebf" } } } ``` ### Step 8: Test Permission Enforcement Now let's test our permission setup by switching between different user accounts. #### Test 1: Financial Analyst tries to create a file (should fail) First, logout and login as Bob (Financial Analyst): ```bash ph login --logout ph login # Login with Bob's wallet (0xbob...analyst) ph access-token --expiry 7d ``` **Mutation:** ```graphql mutation TodoList_createDocument($name: String!) { TodoList_createDocument(name: $name) } ``` **Variables:** ```json { "name": "Q1 Budget Planning" } ``` **Expected Result:** โŒ **Permission Denied** ```json { "errors": [ { "message": "Forbidden: insufficient permissions to create documents" } ] } ``` #### Test 2: Finance Manager creates a file (should succeed) Logout and login as Alice (Finance Manager): ```bash ph login --logout ph login # Login with Alice's wallet (0xalice...finance) ph access-token --expiry 7d ``` **Mutation:** ```graphql mutation TodoList_createDocument($name: String!, $driveId: String) { TodoList_createDocument(name: $name, driveId: $driveId) } ``` **Variables:** ```json { "name": "Q1 Budget Planning", "driveId": "drive-uuid-1234-5678-abcd" } ``` **Expected Result:** โœ… **Success** ```json { "data": { "TodoList_createDocument": "document-uuid-abcd-1234-efgh" } } ``` #### Test 3: Financial Analyst adds a todo to the file (should succeed) Switch back to Bob (Financial Analyst): ```bash ph login --logout ph login # Login with Bob's wallet ph access-token --expiry 7d ``` **Mutation:** ```graphql mutation TodoList_addTodoItem( $docId: PHID $driveId: String $input: TodoList_AddTodoItemInput ) { TodoList_addTodoItem(docId: $docId, driveId: $driveId, input: $input) } ``` **Variables:** ```json { "docId": "document-uuid-abcd-1234-efgh", "driveId": "drive-uuid-1234-5678-abcd", "input": { "text": "confirm runway expectations" } } ``` **Expected Result:** โœ… **Success** ```json { "data": { "TodoList_addTodoItem": 1 } } ``` **Query the todo-List to verify** ```graphql query GetDocument($docId: PHID!, $driveId: PHID) { TodoList { getDocument(docId: $docId, driveId: $driveId) { id name operations { inputText } } } } ``` **Variables:** ```json { "docId": "document-uuid-abcd-1234-efgh", "driveId": "drive-uuid-1234-5678-abcd" } ``` **Expected Result:** ```json { "data": { "TodoList": { "getDocument": { "id": "23fe9b72-6540-4eee-b55e-87e414429dd2", "name": "Q1 Budget Planning" } } } } ``` ### Summary This scenario demonstrates: 1. **Group-based permissions**: Both finance team members have WRITE access to the finance drive through group membership 2. **Operation-level permissions**: Only the Finance Manager can create new financial documents (`AddFile` operation) 3. **Permission inheritance**: Once a document exists, all team members can perform other operations (like renaming) due to their WRITE permissions on the parent drive 4. **Granular control**: You can restrict specific operations while allowing broader document access, perfect for sensitive financial data
Advanced Scenario: Todo List Collaboration This scenario demonstrates advanced authorization patterns for managing contributor access levels to a shared todo list document, focusing on role-based access control and operation-level permissions using the @powerhousedao/todo-demo package. To get access to this subgraph yourself make sure to install the package in your project with `ph install @powerhousedao/todo-demo`
Todo Document Schema Reference ```graphql type TodoList implements IDocument { id: String! name: String! documentType: String! operations(skip: Int, first: Int): [Operation!]! revision: Int! createdAtUtcIso: DateTime! lastModifiedAtUtcIso: DateTime! initialState: TodoList_TodoListState! state: TodoList_TodoListState! stateJSON: JSONObject } """ Module: Todos """ input TodoList_AddTodoItemInput { text: String! } input TodoList_DeleteTodoItemInput { id: OID! } type TodoList_TodoItem { id: OID! text: String! checked: Boolean! } type TodoList_TodoListState { items: [TodoList_TodoItem!]! } input TodoList_UpdateTodoItemInput { id: OID! text: String checked: Boolean } """ Queries: TodoList Document """ type TodoListQueries { getDocument(docId: PHID!, driveId: PHID): TodoList getDocuments(driveId: String!): [TodoList!] } ```
### The Setup - **Project Lead** (Alice): Full admin control - **Core Contributors** (Bob, Carol): Can add and update todo items - **External Contributors** (Dave, Eve): Read-only access, can only add new todo items - **Team Leads** (Frank): Special access to delete todo items only ### Step 1: Create Role-Based Groups **Create role-based groups:** ```graphql mutation CreateCoreGroup { createGroup( name: "core-contributors" description: "Team members who can add and update todo items" ) { id name } } ``` ```graphql mutation CreateExternalGroup { createGroup( name: "external-contributors" description: "External users who can only add new todo items" ) { id name } } ``` ```graphql mutation CreateLeadsGroup { createGroup( name: "team-leads" description: "Team leads who can delete todo items" ) { id name } } ``` ### Step 2: Assign Users to Groups **Add core team members:** ```graphql mutation AddBobToCore { addUserToGroup(userAddress: "0xbob...core", groupId: 1) } ``` ```graphql mutation AddCarolToCore { addUserToGroup(userAddress: "0xcarol...core", groupId: 1) } ``` **Add external contributors:** ```graphql mutation AddDaveToExternal { addUserToGroup(userAddress: "0xdave...external", groupId: 2) } ``` **Add team lead:** ```graphql mutation AddFrankToLeads { addUserToGroup(userAddress: "0xfrank...lead", groupId: 3) } ``` ### Step 3: Set Document-Level Permissions Create a todo document for the next steps. **Grant different access levels to each group for the todo list document:** ```graphql mutation GrantCoreWriteAccess { grantGroupPermission( documentId: "todo-document-id" groupId: 1 permission: WRITE ) { groupId permission group { name } } } ``` ```graphql mutation GrantExternalReadAccess { grantGroupPermission( documentId: "todo-document-id" groupId: 2 permission: READ ) { groupId permission group { name } } } ``` ```graphql mutation GrantLeadsReadAccess { grantGroupPermission( documentId: "todo-document-id" groupId: 3 permission: READ ) { groupId permission group { name } } } ``` ### Step 4: Operation-Level Permission Control **External contributors can only add new todo items:** ```graphql mutation AllowExternalAddTodo { grantGroupOperationPermission( documentId: "todo-document-id" operationType: "AddTodoItem" groupId: 2 ) { operationType group { name } } } ``` **Only core contributors can update todo items:** ```graphql mutation AllowCoreUpdateTodo { grantGroupOperationPermission( documentId: "todo-document-id" operationType: "UpdateTodoItem" groupId: 1 ) { operationType group { name } } } ``` **Only team leads can delete todo items:** ```graphql mutation AllowLeadsDeletion { grantGroupOperationPermission( documentId: "todo-document-id" operationType: "DeleteTodoItem" groupId: 3 ) { operationType group { name } } } ``` ### Step 5: Permission Auditing & Verification **Check what permissions a user has:** ```graphql query MyPermissions { userDocumentPermissions { documentId permission grantedBy createdAt } } ``` **Audit all access to sensitive document:** ```graphql query AuditDocumentAccess($docId: String!) { documentAccess(documentId: $docId) { permissions { userAddress permission grantedBy } groupPermissions { group { name members } permission grantedBy } } } ``` **Verify if user can perform specific operation:** ```graphql query CheckOperationAccess($docId: String!, $operation: String!) { canExecuteOperation(documentId: $docId, operationType: $operation) } ``` ### Step 7: Dynamic Permission Management **Promote external contributor to core team:** ```graphql mutation PromoteContributor($userAddress: String!) { removeUserFromGroup(userAddress: $userAddress, groupId: 2) } ``` ```graphql mutation AddToCore($userAddress: String!) { addUserToGroup(userAddress: $userAddress, groupId: 1) } ``` **Remove user from all groups (suspend access):** ```graphql mutation SuspendUser($userAddress: String!) { removeUserFromGroup(userAddress: $userAddress, groupId: 1) } ``` ### Authorization Patterns Demonstrated 1. **Role-Based Access Control (RBAC)**: Groups represent roles with different permission levels 2. **Document-Level Permissions**: Fine-grained control over specific document access 3. **Operation-Level Granularity**: Fine-grained control over specific todo list operations (add vs. update vs. delete) 4. **Individual Overrides**: User-specific permissions that supersede group permissions 5. **Audit Trail**: Complete visibility into who granted what permissions when 6. **Dynamic Role Management**: Users can be promoted/demoted between roles 7. **Principle of Least Privilege**: Each role gets minimum necessary permissions This scenario showcases how to implement granular permission control for collaborative document editing using specific document model operations from installed packages like @powerhousedao/todo-demo.
## Integration with Auth Flow The document permission system integrates with the **Renown authentication flow**. Renown provides decentralized identity using Ethereum addresses and DIDs (Decentralized Identifiers). ### Authentication Flow 1. **User authenticates via `ph login`**: The user connects their Ethereum wallet and creates/retrieves a DID 2. **User generates a bearer token via `ph access-token`**: A JWT token is created that includes the user's Ethereum address 3. **Bearer token is included in API requests**: The client includes the token in the `Authorization` header 4. **Reactor API verifies the token**: The API validates the JWT and extracts the user's Ethereum address 5. **Permission checks are performed**: The system uses the Ethereum address to look up document permissions ### Example: Making Authenticated Requests ```bash # Generate access token (valid for 7 days) TOKEN="$(ph access-token --expiry 7d)" # Make authenticated request to query your permissions curl -X POST http://localhost:4001/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${TOKEN}" \ -d '{"query": "{ userDocumentPermissions { documentId permission } }"}' ``` **TIP:** Permissions are tied to **Ethereum addresses** (e.g., `0x123...`), not usernames. When granting permissions, use the user's Ethereum address as shown in their Renown profile or obtained from `ph login --status`. ## Best Practices 1. **Use groups for team access**: Instead of granting individual permissions, create groups and assign permissions to groups 2. **Grant ADMIN sparingly**: Only grant ADMIN permission to users who need to manage permissions 3. **Use permission inheritance**: Grant permissions at the drive or folder level, let them inherit down 4. **Document your permission structure**: Keep track of which groups have access to which drives 5. **Regular audits**: Periodically review permissions and remove stale access ## Troubleshooting Common issues and how to resolve them: ### Permission denied errors If a user receives "Permission denied" when trying to access a document: 1. Verify `AUTH_ENABLED=true` and `DOCUMENT_PERMISSIONS_ENABLED=true` in your Reactor configuration 2. Check that the user has a valid Renown credential (`ph login --status`) 3. Verify the bearer token is valid and not expired (`ph access-token` to generate a new one) 4. Check if the user has direct or group permission on the document using `documentAccess` query 5. Check if permission is inherited from a parent document (drive or folder) ### Migrations not running Ensure database migrations have run: ```bash # Migrations run automatically on startup when DOCUMENT_PERMISSIONS_ENABLED=true # Check logs for "Document permission migrations completed" ``` --- ## Signing > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/Authorization/Signing Powerhouse uses two complementary signing mechanisms to establish trust across the document lifecycle: - **Header signing** ties a document's identity to its creator via a cryptographic signature that becomes the document ID. - **Action signing** ensures every mutation to a document is attributable to a specific user and app, and can be verified offline. Both mechanisms use **ECDSA with the P-256 curve and SHA-256** via the Web Crypto API. ## Header Signing (Document Identity) Every Powerhouse document has a header containing immutable identity fields. The document's `id` field is itself a cryptographic signature, meaning the document's identity is inseparable from its creator. ### How it works When a document is created, the system: 1. Generates a **presigned header** with placeholder values via `createPresignedHeader()`. 2. Builds a deterministic payload from the signing parameters: `documentType + createdAtUtcIso + nonce`. 3. Signs that payload with the creator's private key. 4. Sets the resulting signature as the document's `id`. The header stores everything needed for self-contained verification: | Field | Purpose | | ------------------------ | -------------------------------------------------- | | `header.id` | The cryptographic signature (also the document ID) | | `header.sig.publicKey` | The creator's public key (JWK format) | | `header.sig.nonce` | Random nonce used as salt during signing | | `header.documentType` | The document model type | | `header.createdAtUtcIso` | Creation timestamp | ### Verification Anyone can verify a document's authenticity using only its header. The `validateHeader()` function reconstructs a verification-only signer from the embedded public key, regenerates the payload from `documentType + createdAtUtcIso + nonce`, and verifies the signature matches the document ID. ```typescript // Throws if the header signature is invalid await validateHeader(document.header); ``` ### Key functions | Function | Location | Purpose | | ------------------------------- | ----------------------------------- | ----------------------------------------------------------- | | `createPresignedHeader()` | `document-model/src/core/header.ts` | Creates an unsigned header with placeholder fields | | `createSignedHeader()` | `document-model/src/core/header.ts` | Signs a presigned header, setting `id` to the signature | | `createSignedHeaderForSigner()` | `document-model/src/core/header.ts` | Convenience: creates and signs a header in one step | | `validateHeader()` | `document-model/src/core/header.ts` | Verifies a header's signature using its embedded public key | ## Action Signing (Operation Authenticity) When a user dispatches an action (e.g., editing a field, adding a record), that action can be cryptographically signed to prove who performed it and that the content has not been tampered with. ### The ISigner interface The `ISigner` interface is the core abstraction for all signing operations: ```typescript interface ISigner { user?: UserActionSigner; // { address, networkId, chainId } app?: AppActionSigner; // { name, key } publicKey: CryptoKey; sign(data: Uint8Array): Promise; verify(data: Uint8Array, signature: Uint8Array): Promise; signAction(action: Action, abortSignal?: AbortSignal): Promise; } ``` It serves two purposes: - `sign()` / `verify()` handle raw data signing, used for **header signing**. - `signAction()` produces a structured `Signature` tuple, used for **action signing**. ### The Signature tuple A signed action produces a 5-element `Signature` tuple: ``` [timestamp, signerKey, actionHash, hashField, signatureHex] ``` | Index | Field | Description | | ----- | -------------- | ----------------------------------------------------------------------------------- | | 0 | `timestamp` | Unix timestamp (seconds) when the action was signed | | 1 | `signerKey` | The signer's public key identifier (typically a `did:key` URI) | | 2 | `actionHash` | SHA-1 hash of `documentId + scope + actionType + JSON(input)` | | 3 | `hashField` | Previous state hash, or `prevStateHash:resultingStateHash` for offline verification | | 4 | `signatureHex` | The ECDSA signature bytes as a `0x`-prefixed hex string | The signed message uses the prefix `\x19Signed Operation:\n{length}` followed by the concatenation of elements 0-3, matching the pattern used by Ethereum-style message signing. ### ActionSigner context Each signed action carries an `ActionSigner` context that identifies both the user and the application: ```typescript type ActionSigner = { user: UserActionSigner; // { address, networkId (CAIP-2), chainId (CAIP-10) } app: AppActionSigner; // { name, key (DID) } signatures: Signature[]; }; ``` This context is attached to the action's `context.signer` field and flows through the entire system -- from the client, through the reactor, into storage, and out through the GQL API. ### Key functions | Function | Location | Purpose | | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------ | | `buildOperationSignature()` | `document-model/src/core/actions.ts` | Creates a Signature tuple from an action context | | `buildSignedAction()` | `document-model/src/core/actions.ts` | Reduces an action, signs it, and attaches the signer context | | `verifyOperationSignature()` | `document-model/src/core/actions.ts` | Verifies a Signature tuple against its signer | | `buildOperationSignatureParams()` | `document-model/src/core/crypto.ts` | Builds the 4-element params from action context | | `buildOperationSignatureMessage()` | `document-model/src/core/crypto.ts` | Constructs the prefixed message for signing | ## ReactorClient Auto-Signing The `ReactorClient` automatically signs all actions before submitting them to the reactor. You do not need to manually sign actions when using the client. ### How it works `ReactorClient` holds an `ISigner` instance. Every mutation method -- `execute()`, `create()`, `createChild()`, `add()`, `remove()`, `move()` -- calls `signActions()` internally before submitting to the reactor. If an action already has valid signatures, it is passed through unchanged. ```typescript // From reactor/src/core/utils.ts const signAction = async (action, signer, signal?) => { // Skip if already signed const existingSignatures = action.context?.signer?.signatures; if (existingSignatures && existingSignatures.length > 0) { return action; } const signature = await signer.signAction(action, signal); return { ...action, context: { ...action.context, signer: { user: { address: signer.user?.address || "", ... }, app: { name: signer.app?.name || "", key: signer.app?.key || "" }, signatures: [signature], }, }, }; }; ``` ### Wiring a signer Use `ReactorClientBuilder.withSigner()` to configure signing. It accepts either a bare `ISigner` or a `SignerConfig` that includes an optional verifier: ```typescript // Option 1: Signing only (no server-side verification) const client = await new ReactorClientBuilder() .withReactorBuilder(reactorBuilder) .withSigner(mySigner) .build(); // Option 2: Signing + verification const client = await new ReactorClientBuilder() .withReactorBuilder(reactorBuilder) .withSigner({ signer: mySigner, verifier: createSignatureVerifier(), }) .build(); ``` If no signer is provided, the client defaults to `PassthroughSigner` -- a no-op implementation that returns empty signatures, effectively disabling signing. ### ISigner implementations | Implementation | Package | Purpose | | -------------------- | --------- | ----------------------------------------------------------- | | `PassthroughSigner` | `reactor` | No-op signer, used when signing is disabled | | `RenownCryptoSigner` | `renown` | Production signer using ECDSA P-256 with `did:key` identity | `RenownCryptoSigner` is the standard production implementation. It derives signing keys from the Renown identity system and identifies signers using DID URIs (`did:key:z...`). ## Signature Verification Signature verification is optional and runs in the reactor's executor before actions are processed. ### How it works The `SignatureVerifier` class sits in the executor pipeline. When a `SignatureVerificationHandler` is configured, it: 1. Inspects each incoming action for a `context.signer`. 2. If a signer is present but has no signatures, the action is rejected. 3. Calls the handler to verify the signature against the signer's public key. 4. Throws `InvalidSignatureError` if verification fails. This applies to both action jobs (new mutations) and load jobs (operations arriving from sync). ### Configuration Verification is enabled by passing a `verifier` in the `SignerConfig`: ```typescript const client = await new ReactorClientBuilder() .withReactorBuilder(reactorBuilder) .withSigner({ signer: mySigner, verifier: createSignatureVerifier(), }) .build(); ``` The `createSignatureVerifier()` function from the `renown` package returns a handler that uses the Web Crypto API to verify ECDSA P-256 signatures. It extracts the public key from the signer's DID and verifies the signature against the reconstructed message. If no verifier is provided, all actions are accepted regardless of their signature status. ## Signing at the GQL / Switchboard Level When you interact with a reactor through its GraphQL API (the switchboard), signing is handled for you depending on how actions are submitted. ### Submitting actions via GQL mutations The GQL mutations `mutateDocument` and `mutateDocumentAsync` accept actions as JSON objects. These actions are passed through the switchboard's `ReactorClient`, which auto-signs them using whatever `ISigner` was configured on that reactor instance. This means: - **If you submit unsigned actions** through the GQL API, the switchboard signs them on your behalf using its configured signer. - **If you submit pre-signed actions** (actions that already have a `context.signer` with signatures), the switchboard passes them through unchanged -- it does not re-sign. ### When the switchboard signs The switchboard signs actions during any mutation that flows through the `ReactorClient`: | Mutation | What gets signed | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `createDocument` | CREATE_DOCUMENT + UPGRADE_DOCUMENT actions, plus parent relationship action if a parent is specified | | `createEmptyDocument` | Same as above, using a default initial state | | `mutateDocument` / `mutateDocumentAsync` | All submitted actions | | `addRelationship` / `removeRelationship` / `moveRelationship` | Relationship actions on the source document(s) | | `deleteDocument` / `deleteDocuments` | DELETE_DOCUMENT actions for the target and its descendants | ### When you should pre-sign If your client has its own `ISigner` (e.g., a `RenownCryptoSigner` tied to a specific user identity), you should sign actions before submitting them to the GQL API. This ensures the signatures reflect the actual user who performed the action, rather than the switchboard's server-side identity. Pre-signed actions are detected by checking for existing signatures in `action.context.signer.signatures` -- if any are present, the `ReactorClient` skips signing. --- ## Reactor API Authorization > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/Authorization/Authorization **WARNING:** This documentation is still being written and may be incomplete. This guide explains how to configure **authorization** for the Powerhouse Reactor API. Authorization controls who can access your Reactor and what they can do. ## Introduction The **Reactor API** is the API interface to a Powerhouse **Reactor**โ€”a storage node responsible for storing documents, resolving conflicts, and verifying document event histories. Before users can interact with documents in your Reactor, they must pass through authorization checks. ### Authorization Model The Reactor API uses a layered authorization model: | Component | Purpose | Scope | | ------------------------------------------------------- | ---------------------------------------------------------- | ------------ | | **Supreme Admin** (`ADMINS` env var) | Full bypass of all permission checks | Reactor-wide | | **Document Protection** | Determines whether a document requires explicit grants | Per-document | | **[Document Permissions](../docs/docs/02-DocumentPermissions.md)** | Fine-grained READ/WRITE/ADMIN grants on specific documents | Per-document | **TIP:** - For simple setups, configure `ADMINS` and leave documents **unprotected** โ€” any authenticated user can read and write - When you need fine-grained control, **protect** specific documents and manage access with [document permissions](../docs/docs/02-DocumentPermissions.md) ::: ## Prerequisites Before configuring authorization, you need: 1. **A running Reactor**: See [Document Permissions - Starting the Reactor API](../docs/docs/02-DocumentPermissions.md#starting-the-reactor-api) for setup instructions 2. **User Ethereum addresses**: Addresses of users you want to grant admin access to ## Basic Configuration The Reactor API supports two main ways to configure authorization: 1. Using environment variables 2. Using the powerhouse configuration file ### Environment Variables Configure authorization using environment variables before starting the Reactor: ```bash # Required: Enable/disable authentication AUTH_ENABLED=true # Optional: Comma-separated list of admin wallet addresses (full access, bypasses all checks) ADMINS="0x123...,0x456..." # Optional: Make all new documents protected by default (requires explicit grants) DEFAULT_PROTECTION=true # Optional: Enable per-document permission management DOCUMENT_PERMISSIONS_ENABLED=true ``` **Important notes:** - Use full Ethereum addresses (e.g., `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb`) - **ADMINS**: These addresses bypass all permission checks entirely - **DEFAULT_PROTECTION=true**: New documents are created as protected, requiring explicit grants for access - **DEFAULT_PROTECTION=false** (default): New documents are unprotected โ€” anyone can read, authenticated users can write - **DOCUMENT_PERMISSIONS_ENABLED=true**: Enables the per-document permission system for managing grants - If `AUTH_ENABLED=false`, all authorization checks are bypassed (not recommended for production) ### Powerhouse configuration Alternatively, you can configure authorization in your `powerhouse.config.json`: ```json { "auth": { "enabled": true, "admins": ["0x123", "0x456"] } } ``` ## How Authorization Works ### Supreme Admin Addresses listed in `ADMINS` have **full access** to the entire Reactor. They bypass all permission checks, including document protection. Use this for system administrators and project owners. ### Document Protection Each document (drive, folder, or document) can be either **protected** or **unprotected**: | State | Read Access | Write Access | | --------------- | ------------------------------- | ------------------------------- | | **Unprotected** | Anyone | Any authenticated user | | **Protected** | Only users with explicit grants | Only users with explicit grants | - Use `DEFAULT_PROTECTION=true` to make all new documents protected by default - Protection can also be toggled per-document via the GraphQL API ### Per-Document Grants When a document is protected, users need explicit grants to access it. Grants are managed through the [Document Permission System](../docs/docs/02-DocumentPermissions.md) and support: - **Direct grants**: Assigned to a specific user address - **Group grants**: Assigned to a group of users - **Inherited grants**: Flow down from parent (drive โ†’ folder โ†’ document) - **Grant levels**: READ, WRITE, or ADMIN ## Docker Configuration When running the Reactor API in Docker, you can pass these configurations as environment variables: ```bash docker run -e AUTH_ENABLED=true \ -e ADMINS="0x123,0x456" \ -e DEFAULT_PROTECTION=true \ -e DOCUMENT_PERMISSIONS_ENABLED=true \ your-reactor-api-image ``` ## Authorization Flow Understanding how authorization checks work: ``` User makes API request | Is AUTH_ENABLED=true? |-- No --> Access granted (no auth required) +-- Yes --> Continue to auth check | Does user have valid bearer token? |-- No --> 401 Unauthorized +-- Yes --> Extract Ethereum address from token | Is user's address in ADMINS? |-- Yes --> Full access (bypass all checks) +-- No --> Check document protection | Is the target document protected? |-- No --> Read: allow, Write: allow (user is authenticated) +-- Yes --> Check grants | Does user have a grant for this document? |-- Yes --> Grant-level access (READ/WRITE/ADMIN) +-- No --> 403 Forbidden ``` ### Step-by-Step 1. **Authentication check**: If `AUTH_ENABLED=false`, all requests are allowed (skip remaining steps) 2. **Token validation**: User must include valid bearer token from `ph access-token` 3. **Supreme admin check**: If the user's address is in `ADMINS`, grant full access 4. **Document protection check**: If the document is unprotected, allow read for everyone and write for authenticated users 5. **Grant check** (protected documents only): Check [document-level permissions](../docs/docs/02-DocumentPermissions.md) for READ/WRITE/ADMIN grants **TIP:** - **Authentication** ([Renown flow](../docs/docs/01-RenownAuthenticationFlow.md)): Proves who you are (`ph login`) - **Authorization** (this guide): Determines what you can access ::: ## Common Scenarios ### Scenario 1: Open Development Environment You want a local Reactor where anyone can access everything: ```bash AUTH_ENABLED=false ``` **Use case**: Local development, testing, demos ### Scenario 2: Protected Production Environment Only admins have unrestricted access, all documents require explicit grants: ```bash AUTH_ENABLED=true ADMINS="0xProjectLead" DEFAULT_PROTECTION=true DOCUMENT_PERMISSIONS_ENABLED=true ``` **Use case**: Private team workspace, sensitive data. Use [document permissions](../docs/docs/02-DocumentPermissions.md) to grant access to specific users. ### Scenario 3: Open Collaboration with Admin Oversight Any authenticated user can read and write, admins manage the Reactor: ```bash AUTH_ENABLED=true ADMINS="0xProjectLead" ``` **Use case**: Open source projects, community collaboration. Documents are unprotected by default, so any authenticated user can contribute. Protect specific documents as needed. ### Scenario 4: Mixed Access Some documents are open, others are restricted: ```bash AUTH_ENABLED=true ADMINS="0xProjectLead" DOCUMENT_PERMISSIONS_ENABLED=true ``` **Use case**: Team workspace where most content is open but certain drives/documents are protected with explicit grants. ## Security Best Practices 1. **Secure admin addresses**: Keep admin wallet private keys secure 2. **Use HTTPS in production**: Never expose the Reactor API over plain HTTP 3. **Regular access audits**: Review admin list and document grants periodically 4. **Development vs Production**: Use `AUTH_ENABLED=false` only in local development 5. **Protect sensitive documents**: Use `DEFAULT_PROTECTION=true` or protect individual documents containing sensitive data 6. **Use document permissions**: For production, combine admin access with [document-level permissions](../docs/docs/02-DocumentPermissions.md) for fine-grained control ## Troubleshooting Common issues and solutions: ### "403 Forbidden" Error **Problem**: User has valid token but cannot access a document **Solutions**: 1. Check if the document is protected โ€” if so, the user needs an explicit grant 2. Verify the user's address is in `ADMINS` if they need full access 3. Use [document permissions](../docs/docs/02-DocumentPermissions.md) to grant the user access to the specific document 4. Verify wallet addresses are correctly formatted (full addresses, no typos) ### "401 Unauthorized" Error **Problem**: Request is rejected before reaching authorization **Solutions**: 1. User needs to authenticate: `ph login` 2. Generate valid bearer token: `ph access-token` 3. Include token in request header: `Authorization: Bearer ` 4. Check token hasn't expired ### Configuration Not Taking Effect **Problem**: Changed configuration but nothing happens **Solutions**: 1. Restart the Reactor after configuration changes 2. Check environment variables are set in the correct shell/environment 3. Verify `powerhouse.config.json` syntax is valid JSON 4. Environment variables override config file - check for conflicts ### How to Find a User's Ethereum Address ```bash # User checks their own address ph login --status ``` The address is shown in the authentication status output. ## Next Steps - **Document Permissions**: Set up [fine-grained document permissions](../docs/docs/02-DocumentPermissions.md) - **Authentication**: Learn about the [Renown authentication flow](../docs/docs/01-RenownAuthenticationFlow.md) - **API Usage**: Explore [using the GraphQL API](../docs/../04-WorkWithData/02-UsingTheAPI.mdx) --- ## Configure a drive > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/ConfiguringDrives A drive in Powerhouse is a container for documents and data. It's a place where you can organize and store your documents and share them with others. This guide walks you through configuring and managing drives in your Powerhouse environment. **INFO:** Before configuring a drive, ensure you have: - Powerhouse [CLI installed](/academy/MasteryTrack/BuilderEnvironment/BuilderTools) - Access to a Powerhouse instance - Appropriate permissions to create and manage drives ::: ## Understanding drives ### Local drives A local drive is a container for local documents and data, hosted on your local machine. Technically, a drive is itself a document that contains a list of the documents inside it. When you run Connect locally with `ph connect`, a local drive is automatically added. You can also create a new local drive by clicking **'Create New Drive'** in Connect. ### Remote drives vs. reactors Remote drives in Powerhouse allow you to connect to and work with data stored in external systems or cloud services. These drives act as bridges between Powerhouse contributors or other data sources, enabling seamless data synchronization. Drives can exist in three types of locations: - **Local Storage**: For offline or on-device access. - **Cloud Storage**: For centralized, scalable data management. - **Decentralized Storage**: Such as Ceramic or IPFS, enabling distributed and blockchain-based storage options. **TIP:** **Powerhouse Reactors** are the nodes in the network that store and synchronize documents and drives, resolve conflicts, and rerun operations to verify document event histories. Reactors can be configured for local storage, centralized cloud storage, or a decentralized storage network. A reactor allows you to store multiple documents and host **drives** and Drive-apps with different organizational purposes, users, access rights, and more. A drive uses a reactor and its underlying storage layer. A reactor is the low-level component that enables the synchronization of documents and drives. ### Drive-apps **Drive-apps** are specialized interfaces that enhance how users interact with documents within a drive. As mentioned, a drive is technically just another document containing a list of other documents. This means you can create a custom editor for your drive document. These customized editors are called Drive-apps. They provide custom views, organization tools, and interactive features tailored to specific use cases. For example, a Drive-app might present data as a Kanban board, provide aggregated insights, or offer specialized widgets for data processing. To learn more about building and customizing Drive-apps, check out our [Building a Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) guide. ## Creating a new drive
Create a new drive
The drive management modal after clicking the 'Create New Drive' button.
To create a new drive in Powerhouse, follow these steps: 1. Click the "**Create New Drive**" button in the Connect interface or the **+** icon in the Connect sidebar. 2. In the modal that appears, enter a name for your drive in the "**Drive Name**" field. 3. Select the desired Drive-app (such as the Generic Drive-app, or any other Drive-app you've installed). 4. Choose the location for your drive: **Local** (only available to you), **Cloud** (available to people in this drive), or **Public** (available to everyone). 5. (Optional) Enable the "Make available offline" toggle if you want to keep a local backup of your drive. 6. Once all options are set, click the "Create new drive" button to finalize and create your drive. ## Add a new remote drive via GraphQL mutation You can also add a new remote drive to your Connect environment programmatically using GraphQL mutations. This is especially useful for automation, scripting, or integrating with external systems. **INFO:** - Access to the Switchboard or remote reactor (server node) of your Connect instance. In your local project, you can start your Reactor by running the following command in a different terminal from Connect Studio. `bash ph reactor ` - The GraphQL endpoint of your instance. For example, for the staging environment, use: `https://staging.switchboard.phd/graphql` (this is a supergraph gateway. Read more about [subgraphs and supergraphs here](/academy/MasteryTrack/WorkWithData/UsingSubgraphs). - Appropriate permissions to perform mutations. :::
Create a new drive
The GraphQL interface for creating a new drive through a mutation.
### 1. **Navigate to the GraphQL Playground or use a GraphQL client** - Open [https://switchboard.phd/graphql](https://switchboard.phd/graphql) in your browser, or use a tool like [GraphQL Playground](https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/). ### 2. **Prepare the Mutation** - Use the following mutation to create a new drive. Since a drive is itself a document of type `DocumentDrive`, you use the `DocumentDrive` mutation namespace: ```graphql title="Create Drive Mutation" mutation CreateDrive($name: String!, $slug: String) { DocumentDrive { createDocument(name: $name, slug: $slug) { id name slug documentType state { global { name icon } } } } } ``` - These are the example variables, feel free to change these as you like: ```json title="Example Variables" { "name": "AcademyTest", "slug": null } ``` - You can also provide a custom `slug` if needed. ### 3. **Execute the Mutation** - Run the mutation. On success, you will receive a response containing the new drive's details: ```json title="Successful Response" { "data": { "DocumentDrive": { "createDocument": { "id": "6461580b-d317-4596-942d-f6b3d1bfc8fd", "name": "AcademyTest", "slug": "6461580b-d317-4596-942d-f6b3d1bfc8fd", "documentType": "powerhouse/document-drive", "state": { "global": { "name": "AcademyTest", "icon": null } } } } } } ``` ### 4. **Construct the Drive URL** - Once you have the `id` or `slug`, you can construct the drive URL for Connect: - Format: `domain/d/` or `domain/d/` - Depending on whether you are using a hosted or a local environment, the domain in your URL will change. - Example: `https://connect.phd/d/6461580b-d317-4596-942d-f6b3d1bfc8fd` - Example: `https://localhost:4001/d/6461580b-d317-4596-942d-f6b3d1bfc8fd` ### 5. **Add the Drive in Connect** - Use the constructed URL to add or access the drive in your Connect environment via the 'Add Drive' button.
Create a new drive
The 'Add Drive' button that allows you to enter your constructed Drive URL.
--- This approach allows you to automate drive creation and integration with other systems, making it easy to manage drives at scale. ## Up next You've now experienced the use of GraphQL to modify or read data captured in Powerhouse for the first time. You can now either continue with: - User interfaces and [build custom Drive-apps](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) - Keep playing with data and the [Switchboard API](/academy/MasteryTrack/WorkWithData/UsingTheAPI) Enjoy! --- ## Using the API > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/UsingTheAPI ## Introduction to Switchboard **Switchboard** is the API interface that enables developers and data engineers to access data captured through document models in Connect and Fusion. Once you've structured and captured data from your business processes, Switchboard allows you to leverage that data to build insightful experiences in external websites, create interactive drive widgets, or generate detailed reports and dashboards in Fusion. **TIP:** Since your document models are defined with a GraphQL schema, you can use the same objects and fields in your queries and mutations to retrieve or write data from and to your documents.
**New to GraphQL?** Click here for a primer on GraphQL concepts at Powerhouse GraphQL plays a fundamental role in defining document model data schemas, handling data access patterns, and enabling event-driven workflows within the Powerhouse ecosystem. More specifically, GraphQL is used as: - The **schema definition language (SDL)** for defining our document models and thereby self-documenting the API to the data model. It allows developers to define the structure and relationships of data in a strongly-typed format. - As the **query language in subgraphs**, which allow different services to expose and query structured data dynamically. GraphQL SDL is the backbone of **Specification Driven Design & Development** at Powerhouse. By defining your document models in SDL, you create machine-readable specifications that serve as a shared languageโ€”bridging the gap between developers, designers, and AI agents for precise, iterative collaboration. ### Why GraphQL? - **Precision**: Instead of over-fetching or under-fetching data, GraphQL enables you to specify the precise data requirements in your query. - **Single Endpoint**: With GraphQL, you can access all the data you need through one endpoint, reducing the number of network requests. - **Dynamic Queries**: Its introspective nature allows developers to explore the API's schema dynamically, which streamlines development and documentation. ### Schema The schema defines the structure of a GraphQL API. It acts as a contract between the client and server, detailing: - **Data Types**: The various types of data that can be queried. For example the contributor type and the project type - **Fields**: The available fields on each type. For example the contributor type has a field 'name' and the project type has a field 'title' - **Relationships**: How different types relate to each other. For example the contributor type has a relationship with the project type ```graphql title="Example of a Powerhouse Contributor schema in GraphQL" type Contributor { id: ID! name: String! reputationScore: Float projects: [Project] # The Contributor type has a field 'projects' that returns an array of Project objects } type Project { id: ID! title: String! status: String budget: Float } type Query { getContributor(id: ID!): Contributor } ``` With the following query someone can request the contributor with the id 123: ```graphql title="Example of a query to get a contributor" query { getContributor(id: "123") { name reputationScore projects { # Accessing the related projects title status } } } ``` ### Fields and Arguments - **Field**: A specific piece of data you can request from an object. When you build a query, you select the fields you want to retrieve. - **Argument**: Key-value pairs that can be attached to fields to customize and refine the query. Some fields require arguments to work correctly, especially when dealing with mutations. Powerhouse uses invoices as part of its decentralized operations. With GraphQL, an invoice query might look like this. Here, contributorId and status are arguments that filter the results to return only paid invoices for a specific contributor. ```graphql title="Fetching an Invoice with Filtering" query { getInvoices(contributorId: "456", status: "PAID") { id amount currency dueDate } } ``` ### Introspection GraphQL APIs are self-documenting. Through introspection, you can query the API to retrieve details about its schema, including: - The list of **available types and fields**. - The **relationships** between those types. This capability is particularly useful for developing dynamic client applications and auto-generating documentation. Developers might want to see what data structures are available. This makes it easy to explore document models and read models in Powerhouse without needing to consult extensive external documentation. ```graphql title="Discovering Available Queries" { __schema { types { name fields { name } } } } ``` ### Connections, Edges, and Nodes When dealing with lists of data, GraphQL employs a pattern that includes: - **Connection**: A structure that represents a list of related objects. - **Edge**: Represents the link between individual nodes (objects) in a connection. Each edge contains: - A node field (the actual object). - A cursor for pagination. - **Node**: The individual object in the connection. When querying nodes, you continue selecting subfields until all the data resolves to scalar values. To efficiently fetch invoices in Powerhouse, a paginated query could look like this. This allows Powerhouse Switchboard to efficiently handle large datasets and return results incrementally: ```graphql title="Paginated List of Invoices" query { invoices(first: 10, after: "cursor123") { edges { node { id amount dueDate } cursor } pageInfo { hasNextPage endCursor } } } ``` ### Mutations While queries retrieve data, **mutations modify data**. In Powerhouse, a contributor might need to submit an invoice after completing a task. A GraphQL mutation for this could be: ```graphql title="Submitting an Invoice" mutation { submitInvoice( input: { contributorId: "123" amount: 500.00 currency: "USD" dueDate: "2024-03-01" } ) { id status } } ``` ### GraphQL Subgraphs in Powerhouse Powerhouse structures its data into **subgraphs**, which are modular GraphQL services that connect to the Reactor (Powerhouse's core data infrastructure) or Data Stores fueled by data from processors. Each subgraph has its own SDL, ensuring modularity and flexibility while working within the ecosystem. **Fetching data from the Reactor:** Powerhouse uses GraphQL to expose system-level data, such as drives, users, and operational records through the **Reactor Subgraph**, which allows querying of drives, stored files and folders. **Operational data stores:** Custom subgraphs can be created to store and retrieve operational data in real time. For example, a subgraph can track file uploads and expose this data via GraphQL queries. ### CQRS Architecture with GraphQL Powerhouse uses **CQRS (Command Query Responsibility Segregation)** to separate write operations (commands) from read operations (queries). This improves system scalability and flexibility: - **GraphQL Queries** handle read operations, retrieving structured data efficiently - **GraphQL Mutations** handle write operations, modifying the state in a controlled manner Powerhouse's subgraphs act as the read layer, while processors handle write operations into operational data stores. This prevents conflicts between querying and modifying data. | Layer | Role | GraphQL Usage | Implementation | | ---------------------- | --------------------------------------------------- | ----------------- | -------------- | | Write Model (Commands) | Handles state changes (adding, modifying, deleting) | GraphQL Mutations | Processor | | Read Model (Queries) | Optimized for fetching/reading/retrieving data | GraphQL Queries | Subgraph | For more information about GraphQL fundamentals, visit the [Introduction to GraphQL](https://graphql.org/learn/) documentation.
## Querying a document with the GraphQL API ### Starting the reactor locally In this tutorial, we'll show how to use a **GraphQL** query to query a document model. We'll continue with the **To-do List example** from our [introduction tutorial](/academy/GetStarted/CreateNewPowerhouseProject), but the process can be applied to any other document model. To make our document model available in the Apollo Studio Sandbox, we'll need to store it on a remote [Reactor](/academy/Architecture/WorkingWithTheReactor). **INFO:** **Powerhouse Reactors** are the nodes in the network that store documents, resolve conflicts, and rerun operations to verify document event histories. Reactors can be configured for local storage, centralized cloud storage, or a decentralized storage network. Just as you can run Connect locally in studio mode, you can also run a Reactor locally. Use the following command in the terminal from within your Powerhouse project directory: ```bash ph reactor ``` To start both Connect and a Reactor locally at the same time in a Powerhouse project, you can use the following command: ```bash ph dev ``` This will return a URL to access the Reactor. ```bash [Reactor]: โžœ Reactor: http://localhost:4001/d/powerhouse ``` ### Adding a remote drive or Reactor to Connect If the remote drive or Reactor isn't present yet in Connect, you can add it by clicking the (+) 'Create New Drive' button in the Connect drive navigation and using the localhost URL to add a new drive with its underlying reactor. Usually, this is http://localhost:4001/d/powerhouse. Get access to an **organization's drive** instances by adding their drive to your Connect Drive navigation tree view with the help of the correct drive URL. Click the (+) 'Create New Drive' to add a public drive. To add a new drive, you'll have to know the correct public URL of the drive. Read more about [configuring drives](/academy/Architecture/WorkingWithTheReactor).
Add a drive through an URL
The 'Add Drive' button that allows you to enter a Drive URL.
## Query the state of a document Now that we have our remote reactor and/or drive running, we can store our document model on it. Let's quickly create a new to-do list document in Connect Studio to test the process. Let's call it **'Powerhouse-onboarding-tasks'**. Add the following to-dos to your list: - [ ] Sign up for Powerhouse - [ ] Do the work - [ ] Deliver the work - [ ] Send the invoice - [ ] Get paid Below is the operation history of the to-do list document. As you can see, the operations are logged in the order they were executed.
Operation history in Connect
The operation history of the to-do list document, showing each change made.
Now that we have some data in our document model, we can query it through the GraphQL API. ### Option 1: Query your document via the Switchboard API Button. Whenever you want to start a query from a document within Connect, you can open Switchboard by clicking the Switchboard icon in the top right-hand corner of the document editor interface. The Switchboard API button at the top of your document model will get you the complete state of your current document. This will prepopulate the Apollo Studio Sandbox with the correct **DocumentID** for your document model.
Switchboard button in document editor
The Switchboard button provides a direct link to the GraphQL API for the document.
### Option 2: Query your document by document ID In your Document Toolbar, you will find an icon to visit your operations history. At the top of the toolbar, you will find your document ID. Copy this ID to use it in the Switchboard API.
Copy the DocumentID
You can copy your Document ID from your operations history.
When you navigate to your Switchboard endpoint by adding **`/graphql`** to the end of your URL (e.g., http://localhost:4001/graphql, https://switchboard.phd/graphql, or add it to a custom domain), you can use this document ID to query the state of your document. The documentation on the left-hand side of the Apollo Sandbox will show you all the different fields that are available to query.
Apollo Studio with document query
The Apollo Studio Sandbox showing the available fields for querying a document.
### Option 3: Search for your document ID via GraphQL **Alternatively**, we can use our Reactor URL and endpoint to figure out the document ID. e.g., http://localhost:4001/graphql, https://switchboard.phd/graphql, or your custom https://switchboard.domain/graphql We can find out the ID of our document by querying the drive for its documents. Since we only have one document in our drive, this query will return the ID of our to-do list document. ```graphQL title="Document ID Query" query Query { ToDoList { ToDoList_documents { items { id name documentType revisionsList { scope revision } createdAtUtcIso lastModifiedAtUtcIso } } } } ``` This example query is structured to request all documents of type `ToDoList`. It extracts common metadata fields such as **id**, **name**, **documentType**, **revisionsList**, **createdAtUtcIso**, and **lastModifiedAtUtcIso**. ### Get the state of the document Once you've found your document via any of the three options above, you'll be able to query its state. In the previous step, we queried for document metadata. Now let's query for the actual content of the document state. ```graphql title="Get the Document state" query getDocument($identifier: String!) { ToDoList { document(identifier: $identifier) { document { id name createdAtUtcIso lastModifiedAtUtcIso revisionsList { scope revision } state { global { items { id text checked } stats { total checked unchecked } } } } } } } ``` ```json title="Example variables" { "identifier": "03eb6780-f1d7-438c-84a0-6d93dfb8f6af" } ``` This query will return the current state of the document, including all to-do items and stats.
Executing a mutation for a to-do item in Apollo Studio
The Apollo Studio Sandbox showing the addTodoItem mutation. You can see the variables passed in and the response from the server.
## Mutate the state of a document Now that we know how to query the state of a document, we can start to write to it. To perform write operations, we use **GraphQL Mutations**. Mutations are similar to queries, but they are used to create, update, or delete data. For our To-do List, we'll want to add, check, and remove items. ### Adding a new to-do item Let's start by adding a new item to our list. The document model for our to-do list has an `ADD_TODO_ITEM` operation, which translates to an `addTodoItem` mutation in GraphQL. To use this mutation, you need to provide the `docId` of the to-do list you want to modify, and the `text` and `id` for the new to-do item. We'll specify these via variables. Here is an example of how to structure the mutation: ```graphql title="example-add-mutation" mutation AddTodoItem($docId: PHID!, $input: ToDoList_AddTodoItemInput!) { ToDoList { addTodoItem(docId: $docId, input: $input) { id name revisionsList { scope revision } } } } ``` ```json title="example-variables" { "docId": "03eb6780-f1d7-438c-84a0-6d93dfb8f6af", "input": { "text": "My new to-do from GraphQL", "id": "1" } } ``` Replace the example `docId` with the actual ID of your document. You can get this ID by querying as we did before. When you execute this mutation in Apollo Studio, it will add the new item to your to-do list. The response will return the updated document with its new revision. ### Deleting a to-do item To delete an item, you'll need its unique identifier. When you query for the to-do items in your list, each one will have an `id`. You'll use this `id` to specify which item to delete. The document model provides a `DELETE_TODO_ITEM` operation, which corresponds to a `deleteTodoItem` mutation. Here's how you can use it: ```graphql title="example-delete-mutation" mutation DeleteTodoItem($docId: PHID!, $input: ToDoList_DeleteTodoItemInput!) { ToDoList { deleteTodoItem(docId: $docId, input: $input) { id name revisionsList { scope revision } } } } ``` ```json title="example-variables" { "docId": "03eb6780-f1d7-438c-84a0-6d93dfb8f6af", "input": { "id": "0.6325811781889789" } } ``` Make sure to replace the `docId` and `id` with the appropriate values for your document and the item you wish to delete. After executing this mutation, the specified to-do item will be removed from your list. ### Verifying the changes After performing a write mutation, you can verify that the change was successful in a couple of ways: 1. **Query the document state again:** Rerun the `document` query from earlier in this tutorial. You should see the new item in the list or the deleted item removed. 2. **Check the Operation History:** The operation history in Connect will show the new `ADD_TODO_ITEM` or `DELETE_TODO_ITEM` operation, along with who performed it and when. This provides a complete audit trail of all changes to the document. ### Summary This ability to programmatically read from and write to documents via the GraphQL API is a powerful feature of Powerhouse. It unlocks countless possibilities for integrating your structured data into other applications, building automated workflows, and creating rich, data-driven user experiences. --- ## Using subgraphs > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/UsingSubgraphs This tutorial will demonstrate how to create and customize a subgraph using our to-do list project as an example. Let's start with the basics and gradually add more complex features and functionality. ## What is a subgraph? A subgraph in Powerhouse is a **GraphQL-based modular data component** that extends the functionality of your document models. While document models handle the core state and operations, subgraphs can: 1. Connect to external APIs or databases 2. Add custom queries and mutations 3. Automate interactions between different document models 4. Provide additional backend functionality ### Subgraphs can retrieve data from - **The Reactor** โ€“ The core Powerhouse data system or network node. - **Relational Data Stores** โ€“ Structured data storage for operational processes, offering real-time updates, for querying structured data. - **Analytics Stores** โ€“ Aggregated historical data, useful for insights, reporting and business intelligence. ### Subgraphs consist of - **A schema** โ€” Which defines the GraphQL Queries and Mutations. - **Resolvers** โ€” Which handle data fetching and logic. - **Context Fields** โ€” Additional metadata that helps in resolving data efficiently. #### Additionally, context fields allow resolvers to access extra information, such as: - **User authentication** (e.g., checking if a user is an admin). - **External data sources** (e.g., analytics). ## Example: Implement a search subgraph based on data from the reactor In this example we implement a subgraph which allows to search through todo-list documents in a specific document drive. First we will generate the subgraph with the help of the Powerhouse CLI, then we will define the GraphQL schema and implement the resolvers and finally we will start the reactor and execute a query through the GraphQL Gateway. ### 1. Generate the subgraph Let's start by generating a new subgraph. For our tutorial we will create a new subgraph within our to-do list project you've created in the previous chapters. Open your project and start your terminal. The Powerhouse Vetra toolkit provides a command-line utility to create new subgraphs easily. ```bash title="Run the following command to generate a new subgraph" ph generate subgraph --name search-todos ``` ```bash title="Expected Output" FORCED: ./subgraphs/search-todos/index.ts FORCED: ./subgraphs/search-todos/resolvers.ts FORCED: ./subgraphs/search-todos/schema.ts ``` After generating the subgraph, build your project with a build step. ```bash title="Build your project" pnpm build ``` ### What happened? 1. A new subgraph was created in `./subgraphs/search-todos/` 2. The subgraph was automatically registered in your project's registry 3. Basic boilerplate code was generated with an example query If we now run `ph reactor` we will see the new subgraph being registered during the startup of the Reactor. > Registered /graphql/search-todos subgraph. ``` Initializing Subgraph Manager... > Registered /graphql/auth subgraph. > Registered /graphql/r subgraph. > Registered /graphql/analytics subgraph. > Registered /d/:drive subgraph. > Updating router > Registered /graphql supergraph > Registered /graphql/document-drive subgraph. > Registered /graphql/to-do-list subgraph. > **Registered /graphql/search-todos subgraph.** > Updating router > Registered /graphql supergraph โžœ Reactor: http://localhost:4001/d/powerhouse ``` ## 2. Building a search subgraph Now that we've generated our subgraph its time to define the GraphQL schema and implement the resolvers. **Step 1: Define the schema in `subgraphs/search-todos/schema.ts` by creating the file:** ```typescript export const schema: DocumentNode = gql` """ Subgraph definition """ type Query { searchTodos(driveId: String!, searchTerm: String!): [String!]! } `; ``` **Step 2: Create resolvers in `subgraphs/search-todos/resolvers.ts`:** ```typescript // subgraphs/search-todos/resolvers.ts export const getResolvers = (subgraph: BaseSubgraph) => { const reactorClient = subgraph.reactorClient; return { Query: { searchTodos: async ( parent: unknown, args: { driveId: string; searchTerm: string }, ) => { const children = await reactorClient.getOutgoingRelationships( args.driveId, "child", ); const todoItems: string[] = []; for (const doc of children.results) { if (doc.header.documentType !== "powerhouse/todo-list") { continue; } const todoDoc = doc as TodoListDocument; const amountEntries = todoDoc.state.global.items.filter((e) => e.text.includes(args.searchTerm), ).length; if (amountEntries > 0) { todoItems.push(doc.header.id); } } return todoItems; }, }, }; }; ``` ## 3. Testing the to-do list subgraph ### 3.1. Start the reactor To activate the subgraph, run: ```bash ph reactor ``` You should see the subgraph being registered in the console output: ``` > Registered /graphql/search-todos subgraph. ``` ### 3.2. Create some test data Before testing queries, let's create some to-do list documents with test data: 1. Start Connect ```bash ph connect ``` 1. Open Connect at `http://localhost:3000` in the browser 2. Add the 'remote' drive that is running locally via the (+) 'Add Drive' button. Add 'http://localhost:4001/d/powerhouse' 3. Create a new to-do list document 4. Add some test items: - "Learn about subgraphs" (leave unchecked) - "Build a to-do list subgraph" (mark as checked) - "Test the subgraph" (leave unchecked) ### 3.3. Access GraphQL playground Open your browser and go to: ```bash http://localhost:4001/graphql ``` ### 3.4. Test the queries **Query 1: Search for Todos** ```graphql query { searchTodos(driveId: "powerhouse", searchTerm: "test") } ``` You should get a list of the document Ids which contain the search term "Test". If you want to see the full state of your document use this query. ```graphql query GetDocument($identifier: String!) { ToDoList { document(identifier: $identifier) { document { state { global { items { checked id text } } } } } } } ``` ### 3.5. Test real-time updates To verify that your subgraph stays synchronized with document changes: 1. Keep the GraphQL playground open 2. In another tab, open your to-do list document in Connect 3. Add a new item or check/uncheck an existing item 4. Return to the GraphQL playground and re-run your queries 5. You should see the updated data immediately This demonstrates the real-time synchronization between the document model and the subgraph through event processing. **TIP:** Since you've gotten this far we'll explain a bit more in depth how the GraphQL API or Gateway works! ## 4. Working with the GraphQL Gateway The GraphQL Gateway is a GraphQL schema that combines multiple underlying GraphQL APIs, known as subgraphs, into a single, unified graph. This architecture allows different teams to work independently on their respective services (subgraphs) while providing a single entry point for clients or users to query all available data. ### 4.1 Key concepts - **Subgraph:** An independent GraphQL service with its own schema. Each subgraph typically represents a specific domain or microservice within a larger system. - **Gateway/Router:** A server that sits in front of the subgraphs. It receives client queries, consults the supergraph schema, and routes parts of the query to the relevant subgraphs. It then stitches the results back together before sending the final response to the client. ### 4.2 Benefits of using a supergraph - **Federated Architecture:** Enables a microservices-based approach where different teams can own and operate their services independently. - **Scalability:** Individual subgraphs can be scaled independently based on their specific needs. - **Improved Developer Experience:** Clients interact with a single, consistent GraphQL API, simplifying data fetching and reducing the need to manage multiple endpoints. - **Schema Evolution:** Subgraphs can evolve their schemas independently, and the supergraph can be updated without breaking existing clients, as long as breaking changes are managed carefully. - **Clear Separation of Concerns:** Each subgraph focuses on a specific domain, leading to more maintainable and understandable codebases. ### 4.3 Use the Powerhouse supergraph The Powerhouse supergraph for any given remote drive or reactor can be found under `http://localhost:4001/graphql`. The gateway / supergraph available on `/graphql` combines all the subgraphs, except for the drive subgraph (which is accessible via `/d/:driveId`). To access the endpoint, start the reactor and navigate to the URL with `graphql` appended. The following commands explain how you can test & try the supergraph. - Start the reactor: ```bash ph reactor ``` - Open the GraphQL editor in your browser: ``` http://localhost:4001/graphql ``` The supergraph allows you to both query & mutate data from the same endpoint. **Example: Using the supergraph with to-do list documents** 1. Create a todo document in the `powerhouse` drive using the `ToDoList` `createDocument` mutation: ```graphql mutation CreateTodoList($name: String!, $parentIdentifier: String) { ToDoList { createDocument(name: $name, parentIdentifier: $parentIdentifier) { id name } } } ``` Variables: ```json { "name": "My Test To-do List", "parentIdentifier": "powerhouse" } ``` This returns the document with its ID (e.g., `"abc123"`). Save this ID for the next steps. 2. Add some items to your to-do list using the `addTodoItem` mutation: ```graphql mutation AddTodoItem($docId: PHID!, $input: ToDoList_AddTodoItemInput!) { ToDoList { addTodoItem(docId: $docId, input: $input) { id name revisionsList { scope revision } } } } ``` Variables: ```json { "docId": "abc123", "input": { "text": "Learn about supergraphs" } } ``` This returns the updated document with its new revision. 3. Query the document state using the `document` query: ```graphql query GetTodoList($identifier: String!) { ToDoList { document(identifier: $identifier) { document { id name state { global { items { id text checked } } } } } } } ``` Variables: ```json { "identifier": "abc123" } ``` 4. Use the `searchTodos` subgraph query to search for items across your to-do lists: ```graphql query SearchTodos($driveId: String!, $searchTerm: String!) { searchTodos(driveId: $driveId, searchTerm: $searchTerm) } ``` Variables: ```json { "driveId": "powerhouse", "searchTerm": "supergraph" } ``` This demonstrates how the supergraph provides a unified interface to both your document models and your custom subgraphs, allowing you to query and mutate data from the same endpoint. ## 5. Summary Congratulations! You've successfully built a complete to-do list subgraph that demonstrates the power of extending document models with custom GraphQL functionality. Let's recap what you've accomplished: ### Key concepts learned: - **Subgraphs extend document models** with additional querying and data processing capabilities - **Operational data stores** provide efficient storage for subgraph data - **Event processing** enables real-time synchronization between document models and subgraphs - **The supergraph** unifies multiple subgraphs into a single GraphQL endpoint This tutorial has provided you with a solid foundation for building sophisticated data processing and querying capabilities in the Powerhouse ecosystem. ## Subgraphs are particularly useful for 1. **Cross-Document Interactions**: For example, connecting a to-do list with an Invoice document model: - When an invoice-related task is marked complete, update the invoice status - When an invoice is paid, automatically check off related tasks 2. **External Integrations**: - Sync tasks with external project management tools - Connect with notification systems - Integrate with analytics platforms 3. **Custom Business Logic**: - Implement complex task prioritization - Add automated task assignments - Create custom reporting functionality ### Future enhancements Bridge Processors and Subgraphs โ€” Currently, there's a gap in how processors and subgraphs interact. Powerhouse might improve this in future updates. --- ## Building a processor > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/BuildingAProcessor Processors are components that receive document operations from the reactor and perform side effects. While specialized processor types like [relational database processors](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor) exist, you can build a plain processor by implementing the `IProcessor` interface directly. In this tutorial we will build a **logging processor** that prints a structured summary of every operation to the console. ## What is a processor? A processor implements two methods: - **`onOperations(operations)`** โ€” called when document operations match the processor's filter - **`onDisconnect()`** โ€” called when the processor is disconnected (for cleanup) The reactor calls your processor's `onOperations` method with a list of `OperationWithContext` items. Each item pairs an `Operation` (what happened) with an `OperationContext` (where it happened). ## Key types ```typescript IProcessor, ProcessorFilter, ProcessorRecord, ProcessorFactory, IProcessorHostModule, } from "@powerhousedao/reactor-browser"; ``` **INFO:** `@powerhousedao/reactor-browser` re-exports these types for convenience in browser environments. If you are working outside the browser (Node.js scripts, CLI tools, server-side code), import directly from `@powerhousedao/reactor`. ### `OperationWithContext` Each item your processor receives contains: **`context`** โ€” where the operation happened: | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------------------- | | `documentId` | `string` | The document that was modified | | `documentType` | `string` | e.g. `"powerhouse/todo-list"` | | `scope` | `string` | The scope (e.g. `"global"`, `"local"`) | | `branch` | `string` | The branch (e.g. `"main"`) | | `ordinal` | `number` | Global monotonically increasing ordinal for cross-document ordering | | `resultingState` | `string?` | JSON string of the document state after the operation | **`operation`** โ€” what happened: | Field | Type | Description | | ---------------- | -------- | ---------------------------------------------------- | | `action` | `Action` | Contains `type` (e.g. `"ADD_TODO_ITEM"`) and `input` | | `index` | `number` | Position in the operation history | | `timestampUtcMs` | `string` | When the operation was created | | `hash` | `string` | Hash of the resulting document state | ### `ProcessorFilter` Determines which operations your processor receives. All fields are optional arrays โ€” when provided, operations must match at least one value in each specified field. When a field is omitted, it matches everything. ```typescript type ProcessorFilter = { documentType?: string[]; // e.g. ["powerhouse/todo-list"] scope?: string[]; // e.g. ["global"] branch?: string[]; // e.g. ["main"] documentId?: string[]; // e.g. ["*"] for all documents }; ``` ## 1. Implement the processor Create `processors/operation-logger/index.ts`: ```typescript export class OperationLoggerProcessor implements IProcessor { private driveId: string; constructor(driveId: string) { this.driveId = driveId; console.log(`[OperationLogger] Initialized for drive: ${driveId}`); } async onOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { console.log( `[OperationLogger] drive=${this.driveId}`, `doc=${context.documentId}`, `type=${context.documentType}`, `action=${operation.action.type}`, `index=${operation.index}`, `ordinal=${context.ordinal}`, `scope=${context.scope}`, `branch=${context.branch}`, ); } } async onDisconnect(): Promise { console.log(`[OperationLogger] Disconnected from drive: ${this.driveId}`); } } ``` The processor is straightforward: it receives operations and logs a structured summary for each one. In a real-world scenario, you might send these to an external logging service, a webhook endpoint, or a monitoring system. ## 2. Write the processor factory The factory is responsible for creating processor instances. The reactor calls your factory once per drive. Create `processors/operation-logger/factory.ts`: ```typescript ProcessorRecord, ProcessorFilter, IProcessorHostModule, } from "@powerhousedao/reactor-browser"; export const operationLoggerProcessorFactory = (module: IProcessorHostModule) => async (driveHeader: PHDocumentHeader): Promise => { const filter: ProcessorFilter = { branch: ["main"], documentId: ["*"], scope: ["global"], // Omit documentType to receive operations for ALL document types }; const processor = new OperationLoggerProcessor(driveHeader.id); return [ { processor, filter, }, ]; }; ``` **How the factory pattern works:** 1. The outer function `(module: IProcessorHostModule) => ...` is called once at initialization. The `module` object provides access to shared resources like `analyticsStore`, `relationalDb`, and `config`. 2. The returned function `(driveHeader: PHDocumentHeader) => ProcessorRecord[]` is called once per drive. The `driveHeader` provides access to `driveHeader.id`, `driveHeader.name`, `driveHeader.documentType`, and other header fields. 3. Each factory can return multiple `ProcessorRecord` entries โ€” useful when you want different filters for different aspects of processing (e.g., one processor per document type). ### Filter options explained | Field | Description | | -------------- | ------------------------------------------------------------------ | | `branch` | Which branches to monitor โ€” usually `["main"]` for production data | | `documentId` | Specific document IDs, or `["*"]` for all documents | | `scope` | `["global"]` for shared state, `["local"]` for user-specific state | | `documentType` | Document types to process โ€” omit to match all types | ### Starting position By default, new processors catch up from the beginning โ€” they replay all existing operations. You can change this by setting `startFrom` on the `ProcessorRecord`: ```typescript return [ { processor, filter, startFrom: "current", // Skip historical operations, only process new ones }, ]; ``` ## 3. Register the factory Your processor factory is automatically registered when it is included in your project's processor exports. After creating the files above, make sure they are exported from your `processors/index.ts`: ```typescript export { operationLoggerProcessorFactory } from "./operation-logger/factory.js"; ``` ## 4. Test it Start the reactor: ```bash ph reactor ``` Create or modify documents in the drive. You should see log output like: ``` [OperationLogger] Initialized for drive: powerhouse [OperationLogger] drive=powerhouse doc=abc123 type=powerhouse/todo-list action=ADD_TODO_ITEM index=0 ordinal=1 scope=global branch=main [OperationLogger] drive=powerhouse doc=abc123 type=powerhouse/todo-list action=ADD_TODO_ITEM index=1 ordinal=2 scope=global branch=main ``` ## Summary You've built a plain processor from scratch. The key concepts are: - **`IProcessor`** โ€” the interface your processor implements (`onOperations`, `onDisconnect`) - **`ProcessorFactory`** โ€” a function that creates `ProcessorRecord[]` per drive - **`ProcessorFilter`** โ€” determines which operations your processor receives - **`OperationWithContext`** โ€” the data your processor receives, pairing operations with their document context For processors that need a relational database, see the [Relational Database Processor](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor) tutorial, which builds on these concepts with database migrations and type-safe queries. --- ## Processor best practices > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/ProcessorBestPractices This guide covers advanced patterns for processors that need to mutate documents or query indexed data. It builds on the concepts from [Building a Processor](/academy/MasteryTrack/WorkWithData/BuildingAProcessor) โ€” read that first if you haven't already. **TIP:** Before reading this guide, make sure you're familiar with the basics: - [Building a Processor](/academy/MasteryTrack/WorkWithData/BuildingAProcessor) โ€” processor interface, factories, and filters ## One-way data flow Processors should follow a one-way data flow: operations flow **in** via `onOperations`, and mutations flow **out** via `dispatch`. ![Processor data flow: operations in via onOperations, mutations out via dispatch](../docs/docs/images/processor-data-flow.png) The key principle: **never block on a reactor operation from inside a processor**. When a processor dispatches actions, the job is submitted asynchronously โ€” the processor does not wait for it to complete. If the processor needs to react to the result, it will receive the resulting operations in a future `onOperations` call. This design keeps processors predictable, avoids circular dependencies, and prevents scenarios where a processor waits on work that depends on that same processor finishing. ## Dispatching actions with `dispatch` The `dispatch` API lets a processor mutate documents by executing actions. It is available on the `IProcessorHostModule` object passed to your factory. ```typescript interface IProcessorDispatch { execute( docId: string, branch: string, actions: Action[], signal?: AbortSignal, meta?: Record, ): Promise; } type ProcessorDispatchResult = { id: string; // Job ID status: string; // Job status at time of submission }; ``` Under the hood, `dispatch.execute()` calls `reactorClient.executeAsync()` โ€” it submits the job to the reactor and returns immediately with the job info. The processor does not wait for the job to complete. ### Accessing `dispatch` in a factory Capture `module.dispatch` in your factory and pass it to your processor: ```typescript IProcessor, IProcessorDispatch, IProcessorHostModule, ProcessorRecord, ProcessorFilter, } from "@powerhousedao/reactor-browser"; export const myProcessorFactory = (module: IProcessorHostModule) => async (driveHeader: PHDocumentHeader): Promise => { const processor = new MyProcessor(module.dispatch); const filter: ProcessorFilter = { branch: ["main"], documentId: ["*"], documentType: ["powerhouse/invoice"], scope: ["global"], }; return [{ processor, filter }]; }; ``` ### Example: cross-document automation A processor that watches for approved invoices and dispatches a payment action to a separate document: ```typescript IProcessor, IProcessorDispatch, } from "@powerhousedao/reactor-browser"; export class InvoiceApprovalProcessor implements IProcessor { constructor( private dispatch: IProcessorDispatch, private paymentDocId: string, ) {} async onOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { if ( operation.action.type === "SET_STATUS" && operation.action.input?.status === "approved" ) { await this.dispatch.execute(this.paymentDocId, "main", [ { type: "CREATE_PAYMENT", input: { invoiceId: context.documentId, amount: operation.action.input.amount, }, scope: "global", }, ]); } } } async onDisconnect(): Promise {} } ``` The processor dispatches the `CREATE_PAYMENT` action and moves on. It does not wait for the payment document to be updated. If the processor needs to know the payment was created, it can subscribe to `powerhouse/payment` operations via its filter. ### Parameter reference | Parameter | Type | Required | Description | | --------- | ------------------------- | -------- | --------------------------------------------- | | `docId` | `string` | Yes | Target document ID | | `branch` | `string` | Yes | Branch to apply actions to (usually `"main"`) | | `actions` | `Action[]` | Yes | Actions to dispatch | | `signal` | `AbortSignal` | No | Cancellation signal | | `meta` | `Record` | No | Additional metadata for the job | ### Why not `reactorClient`? **WARNING:** Do not use `reactorClient` methods directly from inside a processor. Use `dispatch` instead. **Why:** 1. **Potential blocking.** Methods like `reactorClient.execute()` wait for the job to reach `READ_READY` status before returning. This means the call could potentially wait on processors to finish their current batch โ€” including the very processor making the call. 2. **Unnecessary round-trip.** The processor will receive the resulting operations through `onOperations` anyway. Waiting for the result synchronously adds latency without benefit. 3. **One-way flow.** Processors should be written with a one-way data flow: react to operations, dispatch new work, and move on. If you need the result of a dispatch, handle it in a future `onOperations` call โ€” don't block waiting for it. **Instead of:** ```typescript // Bad: blocks until job completes const doc = await reactorClient.execute(docId, "main", actions); ``` **Use:** ```typescript // Good: submits and returns immediately await this.dispatch.execute(docId, "main", actions); ``` ## Querying data with `getReadModel` The `getReadModel` API lets a processor look up a registered read model by name. Read models are materialized views maintained by the reactor โ€” they are updated as operations are written and provide efficient query access to indexed data. ```typescript getReadModel(name: string): T ``` Pass the read model's `name` property and cast to the expected type. Throws an error if the read model is not found. ### Default read models The reactor registers several built-in read models that processors can access: | Name | Type | Description | | ----------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------- | | `"document-view"` | `IDocumentView` | Maintains document snapshots for reads; provides `get()`, `find()`, and consistency tracking | | `"document-indexer"` | `IDocumentIndexer` | Tracks document relationships (parent/child) and metadata | | `"processor-manager"` | `IProcessorManager` | Routes operations to registered processors | | `"subscription-notification"` | `SubscriptionNotificationReadModel` | Notifies subscription callbacks when documents change | For example, to look up a document snapshot from inside a processor: ```typescript const documentView = module.getReadModel("document-view"); ``` Custom read models registered via `IReadModelCoordinator` are also available by their `name` property. ### Example: querying indexed data A processor that checks a budget read model before dispatching a payment: ```typescript IProcessor, IProcessorDispatch, } from "@powerhousedao/reactor-browser"; interface IBudgetReadModel { getRemainingBudget(departmentId: string): Promise; } export class BudgetCheckProcessor implements IProcessor { constructor( private dispatch: IProcessorDispatch, private budgetModel: IBudgetReadModel, ) {} async onOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { if (operation.action.type !== "REQUEST_PAYMENT") continue; const { departmentId, amount } = operation.action.input; const remaining = await this.budgetModel.getRemainingBudget(departmentId); if (remaining >= amount) { await this.dispatch.execute(context.documentId, "main", [ { type: "APPROVE_PAYMENT", input: { departmentId, amount }, scope: "global", }, ]); } } } async onDisconnect(): Promise {} } ``` The factory wires the read model: ```typescript export const budgetCheckProcessorFactory = (module: IProcessorHostModule) => async (driveHeader: PHDocumentHeader): Promise => { const budgetModel = module.getReadModel("budget"); const processor = new BudgetCheckProcessor(module.dispatch, budgetModel); return [ { processor, filter: { branch: ["main"], documentId: ["*"], documentType: ["powerhouse/expense-report"], scope: ["global"], }, }, ]; }; ``` **INFO:** `getReadModel` throws if the read model is not registered. Ensure the read model is registered with the reactor before your processor factory runs. If the read model is optional, wrap the call in a try/catch in your factory and handle the missing case gracefully. ## `IProcessorHostModule` reference The `IProcessorHostModule` is passed to your factory's outer function and provides access to all processor APIs: ```typescript interface IProcessorHostModule { analyticsStore: IAnalyticsStore; relationalDb: IRelationalDb; processorApp: ProcessorApp; dispatch: IProcessorDispatch; getReadModel(name: string): T; config?: Map; } ``` | Field | Type | Description | | ---------------- | ------------------------ | ----------------------------------------------------- | | `analyticsStore` | `IAnalyticsStore` | Time-series analytics store for writing metrics | | `relationalDb` | `IRelationalDb` | Relational database for creating namespaced stores | | `processorApp` | `ProcessorApp` | The host application (`"connect"` or `"switchboard"`) | | `dispatch` | `IProcessorDispatch` | Fire-and-forget action dispatch (see above) | | `getReadModel` | `(name: string) => T` | Read model lookup by name (see above) | | `config` | `Map` | Optional processor-specific configuration | **INFO:** `@powerhousedao/reactor-browser` re-exports these types for convenience in browser environments. If you are working outside the browser (Node.js, CLI tools, server-side code), import directly from `@powerhousedao/reactor` or `@powerhousedao/shared`. ## Summary - **Use `dispatch.execute()`** to mutate documents from processors โ€” never `reactorClient` directly - **Use `getReadModel()`** to query indexed data for lookups and validation - **Design for one-way data flow**: operations in via `onOperations`, mutations out via `dispatch` - **Never block** on reactor operations that might circle back to the processor pipeline - **React asynchronously**: if you need the result of a dispatch, handle it in a future `onOperations` call - **Access all APIs** through `IProcessorHostModule` in your factory function ### Related pages - [Building a Processor](/academy/MasteryTrack/WorkWithData/BuildingAProcessor) โ€” processor basics - [Relational Database Processor](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor) โ€” database-backed processors - [Processor Migration Guide](/academy/APIReferences/ProcessorMigrationGuide) โ€” migrating from the legacy strand-based API --- ## Relational database processor > Source: https://powerhouse.academy/academy/MasteryTrack/WorkWithData/RelationalDbProcessor In this chapter, we will implement a **Todo-List** relational database processor. This processor receives operations from the reactor and can use `resultingState` (from the operation context) or data from the operations themselves to populate a database. **What is a Relational Database Processor?** A relational database processor is a specialized component that listens to document changes in your Powerhouse application and transforms that data into a traditional relational database format (like PostgreSQL, MySQL, or SQLite). This is incredibly useful for: - **Analytics and Reporting**: Running complex SQL queries on your document data - **Integration**: Connecting with existing business intelligence tools **TIP:** This tutorial builds on the plain processor concepts covered in [Building a Processor](/academy/MasteryTrack/WorkWithData/BuildingAProcessor). If you are new to processors, start there first. **INFO:** The code examples below import from `@powerhousedao/reactor-browser`, which re-exports all reactor types for convenience in browser environments (editors, drive-apps, subgraphs). If you are working outside the browser โ€” for example in a standalone Node.js script, CLI tool, or server-side processor โ€” import directly from `@powerhousedao/reactor`. ## Generate the Processor To generate a relational database processor, run the following command: ```bash ph generate processor --name todo-indexer --type relationalDb --document-types powerhouse/todo-list ``` **Breaking down this command:** - `--processor todo-indexer`: Creates a processor with the name "todo-indexer" - `--processor-type relationalDb`: Specifies we want a relational database processor (vs other types like analytics or webhook processors) - `--document-types powerhouse/todo-list`: Tells the processor to only listen for changes to documents of type "powerhouse/todo-list" This command creates a processor named `todo-indexer` of type `relational database` that listens for changes from documents of type `powerhouse/todo-list`. **What gets generated:** - A processor class file (`processors/todo-indexer/index.ts`) - A database migration file (`processors/todo-indexer/migrations.ts`) - A factory file for configuration (`processors/todo-indexer/factory.ts`) - A schema file for TypeScript types (`processors/todo-indexer/schema.ts`) ## Define Your Database Schema Next, define your database schema in the `processors/todo-indexer/migration.ts` file. **Understanding Database Migrations** Migrations are version-controlled database changes that ensure your database schema evolves safely over time. They contain: - **`up()` function**: Creates or modifies database structures when the processor starts - **`down()` function**: Safely removes changes when the processor is removed This approach ensures your database schema stays in sync across different environments (development, staging, production). The migration file contains `up` and `down` functions that are called when the processor is added or removed, respectively. In the migration.ts file you'll find an example of the todo table default schema: ```ts export async function up(db: IRelationalDb): Promise { // Create table - this runs when the processor starts await db.schema .createTable("todo") // Creates a new table named "todo" .addColumn("task", "varchar(255)") // Text column for the task description (max 255 characters) .addColumn("status", "boolean") // Boolean column for completion status (true/false) .addPrimaryKeyConstraint("todo_pkey", ["task"]) // Makes "task" the primary key (unique identifier) .ifNotExists() // Only create if table doesn't already exist .execute(); // Execute the SQL command } export async function down(db: IRelationalDb): Promise { // Drop table - this runs when the processor is removed await db.schema.dropTable("todo").execute(); } ``` **Design Considerations:** - We're using `task` as the primary key, which means each task description must be unique - The `varchar(255)` limit ensures reasonable memory usage - The `boolean` status makes it easy to filter completed vs. incomplete tasks - Consider adding timestamps (`created_at`, `updated_at`) for audit trails in production applications ## Generate Database Types After defining your database schema, generate TypeScript types for type-safe queries and better IDE support: ```bash ph generate migration-file --path processors/todo-indexer/migrations.ts ``` **Why Generate Types?** TypeScript types provide several benefits: - **Type Safety**: Catch errors at compile time instead of runtime - **IDE Support**: Get autocomplete and IntelliSense for your database queries - **Documentation**: Types serve as living documentation of your database structure - **Refactoring**: Safe renaming and restructuring of database fields Check your `processors/todo-indexer/schema.ts` file after generation - it will contain the TypeScript types for your database schema. **Example of generated types:** ```ts // This is what gets auto-generated based on your migration export interface Database { todo: { task: string; status: boolean; }; } ``` ## Configure the Processor Filter This give you the opportunity to configure the processor filter in `processors/todo-indexer/factory.ts`: **Understanding Processor Filters** Filters determine which document changes your processor will respond to. This is crucial for performance and functionality: - **Performance**: Only process relevant changes to avoid unnecessary work - **Isolation**: Different processors can handle different document types - **Scalability**: Distribute processing load across multiple processors ```ts ProcessorRecord, IProcessorHostModule, ProcessorFilter, } from "@powerhousedao/reactor-browser"; export const todoIndexerProcessorFactory = (module: IProcessorHostModule) => async ( driveHeader: PHDocumentHeader, processorApp?: ProcessorApp, ): Promise => { // Create a namespace for the processor and the provided drive id // Namespaces prevent data collisions between different drives const namespace = TodoIndexerProcessor.getNamespace(driveHeader.id); // Create a namespaced db for the processor // This ensures each drive gets its own isolated database tables const store = await module.relationalDb.createNamespace( namespace, ); // Create a filter for the processor // This determines which document changes trigger the processor const filter: ProcessorFilter = { branch: ["main"], // Only process changes from the "main" branch documentId: ["*"], // Process changes from any document ID (* = wildcard) documentType: ["powerhouse/todo-list"], // Only process todo-list documents scope: ["global"], // Process global changes (not user-specific) }; // Create the processor instance const processor = new TodoIndexerProcessor(namespace, filter, store); return [ { processor, filter, }, ]; }; ``` **Filter Options Explained:** - **`branch`**: Which document branches to monitor (usually "main" for production data) - **`documentId`**: Specific document IDs to watch ("\*" means all documents) - **`documentType`**: Document types to process (ensures type safety) - **`scope`**: Whether to process global changes or user-specific ones ## Implement the Processor Logic Now implement the actual processor logic in `processors/todo-indexer/index.ts` by copying the code underneath: **Understanding the Processor Lifecycle** The processor has several key methods: - **`initAndUpgrade()`**: Runs once when the processor starts (perfect for running migrations) - **`onOperations()`**: Runs every time relevant document changes occur (this is where the main logic goes) - **`onDisconnect()`**: Cleanup when the processor shuts down **What is `OperationWithContext`?** Processors receive a flat list of `OperationWithContext[]` items. Each item carries both the operation and its context: - **`context`**: `documentId`, `documentType`, `scope`, `branch`, `ordinal` (global ordering), and `resultingState` (JSON string of the document state after the operation) - **`operation`**: `action` (with `type` and `input`), `index`, `timestampUtcMs`, `hash` ```ts export class TodoIndexerProcessor extends RelationalDbProcessor { // Generate a unique namespace for this processor based on the drive ID // This prevents data conflicts between different drives static override getNamespace(driveId: string): string { // Default namespace: `${this.name}_${driveId.replaceAll("-", "_")}` return super.getNamespace(driveId); } // Initialize the processor and run database migrations // This method runs once when the processor starts up override async initAndUpgrade(): Promise { await up(this.relationalDb); // Run the database migration to create tables } // Main processing logic - handles incoming document changes // This method is called whenever there are new document operations override async onOperations( operations: OperationWithContext[], ): Promise { // Early return if no changes to process if (operations.length === 0) { return; } // Process each operation for (const { operation, context } of operations) { // Insert a record for each operation into the database // This is a simple example - you might want more sophisticated logic await this.relationalDb .insertInto("todo") .values({ // Create a unique task identifier combining document ID, operation index, and type task: `${context.documentId}-${operation.index}: ${operation.action.type}`, status: true, // Default to completed status }) // Handle conflicts by doing nothing if the task already exists // This prevents duplicate entries if operations are replayed .onConflict((oc) => oc.column("task").doNothing()) .execute(); // Execute the database query } } // Cleanup method called when the processor disconnects // Use this for closing connections, clearing caches, etc. async onDisconnect() { // Add any cleanup logic here } } ``` ## Expose Data Through a Subgraph ### Generate a Subgraph Generate a new subgraph to expose your processor data: ```bash ph generate subgraph --name todo ``` **What is a Subgraph?** A subgraph is a GraphQL schema that exposes your processed data to clients. It: - Provides a standardized API for accessing your relational database data - Integrates with the Powerhouse supergraph for unified data access - Supports both queries (reading data) and mutations (modifying data) - Can join data across multiple processors and document types ### Configure the Subgraph Open `./subgraphs/todo/schema.ts` and configure the schema: ```ts export const schema: DocumentNode = gql` # Define the structure of a todo item as returned by GraphQL type TodoListEntry { task: String! # The task description (! means required/non-null) status: Boolean! # The completion status (true = done, false = pending) } # Define available queries type Query { todos(driveId: ID!): [TodoListEntry] # Get array of todos for a specific drive } `; ``` Open `./subgraphs/todo/resolvers.ts` and configure the resolvers: ```ts // subgraphs/search-todos/resolvers.ts export const getResolvers = (subgraph: BaseSubgraph) => { const reactorClient = subgraph.reactorClient; const relationalDb = subgraph.relationalDb; return { Query: { todos: { // Resolver function for the "todos" query // Arguments: parent object, query arguments, context, GraphQL info resolve: async (_: any, args: { driveId: string }) => { // Query the database using the processor's static query method // This gives us access to the namespaced database for the specific drive const todos = await TodoIndexerProcessor.query( args.driveId, relationalDb, ) .selectFrom("todo") // Select from the "todo" table .selectAll() // Get all columns .execute(); // Execute the query // Transform database results to match GraphQL schema return todos.map((todo) => ({ task: todo.task, // Map database "task" column to GraphQL "task" field status: todo.status, // Map database "status" column to GraphQL "status" field })); }, }, }, }; }; ``` ## Now query the data via the supergraph. **Understanding the Supergraph** The Powerhouse supergraph is a unified GraphQL endpoint that combines: - **Document Models**: Direct access to your Powerhouse documents - **Subgraphs**: Custom data views from your processors - **Built-in APIs**: System functionality like authentication and drives This unified approach means you can query document state AND processed data in a single request, which is perfect for building rich user interfaces. The Powerhouse supergraph for any given remote drive or reactor can be found under `http://localhost:4001/graphql`. The gateway / supergraph available on `/graphql` combines all the subgraphs, except for the drive subgraph (which is accessible via `/d/:driveId`). To access the endpoint, start the reactor and navigate to the URL with `graphql` appended. The following commands explain how you can test & try the supergraph. - Start the reactor: ```bash ph reactor ``` - This will return an endpoint, but you'll need to change the url of the endpoint to the following URL: ``` http://localhost:4001/graphql ``` The supergraph allows you to both query & mutate data from the same endpoint. Read more about [subgraphs](/academy/MasteryTrack/WorkWithData/UsingSubgraphs)
**Example: Complete Data Flow from Document Operations to Relational Database** **Understanding the Complete Data Pipeline** This example demonstrates the **entire data flow** in a Powerhouse application: 1. **Storage Layer**: Create a drive (document storage container) 2. **Document Layer**: Create a todo document and add operations 3. **Processing Layer**: Watch the relational database processor automatically index changes 4. **API Layer**: Query both original document state AND processed relational data 5. **Analysis**: Compare the different data representations --- ### **Step 1: Create a Drive (Storage Container)** **What's Happening**: Every document needs a "drive" - think of it as a folder or database that contains related documents. This is where your todo documents will live. ```graphql mutation DriveCreation($name: String!) { addDrive(name: $name) { id slug name } } ``` Variables: ```json { "name": "tutorial" } ``` ๐Ÿ’ก **Behind the Scenes**: This creates a new drive namespace. Your relational database processor will create isolated tables for this drive using the namespace pattern we defined earlier. --- ### **Step 2: Create a Todo Document** **What's Happening**: Now we're creating an actual todo list document inside our drive. This uses the document model we built in previous chapters. ```graphql mutation TodoDocument($driveId: String, $name: String!) { TodoList_createDocument(driveId: $driveId, name: $name) } ``` Variables: ```json { "driveId": "fc29ec1b-9934-410b-8682-4731b810d441", "name": "tutorial" } ``` Result: ```json { "data": { "TodoList_createDocument": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b" } } ``` ๐Ÿ’ก **Key Insight**: The returned UUID (`72b73d31-4874-4b71-8cc3-289ed4cfbe2b`) is crucial - this is the document ID that will appear in our processor's database records, linking operations back to their source document. You will receive a different UUID. --- ### **Step 3: Add Todo Items (Generate Operations)** **What's Happening**: Each time we add a todo item, we're creating a new **operation** in the document's history. Our relational database processor is listening for these operations in real-time. ```graphql mutation AddTodo( $driveId: String $docId: PHID $input: TodoList_AddTodoItemInput ) { TodoList_addTodoItem(driveId: $driveId, docId: $docId, input: $input) } ``` Variables: ```json { "driveId": "fc29ec1b-9934-410b-8682-4731b810d441", "docId": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b", "input": { "text": "complete mutation" } } ``` Result: ```json { "data": { "TodoList_addTodoItem": 1 } } ``` ๐Ÿ’ก **What Happens Next**: 1. **Document Model**: Stores the operation and updates document state 2. **Reactor**: Packages the operation as an `OperationWithContext` (with `documentId`, `documentType`, `scope`, etc.) and routes it to matching processors via `onOperations()` 3. **Our Processor**: Automatically receives the `OperationWithContext` and creates a database record 4. **Database**: Now contains: `"72b73d31-4874-4b71-8cc3-289ed4cfbe2b-0: ADD_TODO_ITEM"` ๐Ÿ”„ **Repeat this step 2-3 times** with different todo items to see multiple operations get processed. Each operation will have an incrementing revision number or index --- ### **Step 4: Query Both Data Sources** **The Power of Dual Data Access**: Now we can query BOTH the original document state AND our processed relational data in a single GraphQL request. This demonstrates the flexibility of the Powerhouse architecture. ```graphql query GetTodoList($docId: PHID!, $driveId: PHID) { TodoList { getDocument(docId: $docId, driveId: $driveId) { id name revision state { items { id text checked } } } } } ``` Variables: ```json { "driveId": "fc29ec1b-9934-410b-8682-4731b810d441", "docId": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b" } ``` Response: ```json { "data": { "todos": [ { "task": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b-0: ADD_TODO_ITEM", "status": true }, { "task": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b-1: ADD_TODO_ITEM", "status": true }, { "task": "72b73d31-4874-4b71-8cc3-289ed4cfbe2b-2: ADD_TODO_ITEM", "status": true } ], "TodoList": { "getDocument": [ { "state": { "items": [ { "text": "complete mutation" }, { "text": "add another todo" }, { "text": "Now check the data" } ] } } ] } } } ``` --- ### **๐Ÿ” Data Analysis: Understanding What You're Seeing** **Document Model Data (`ToDoList.getDocuments`):** - โœ… **Current State**: Shows the final todo items as they exist in the document - โœ… **User-Friendly**: Displays actual todo text like "complete mutation" - โœ… **Real-Time**: Always reflects the latest document state - โŒ **Limited History**: Doesn't show how the document changed over time **Processed Relational Data (`todos`):** - โœ… **Operation History**: Shows each individual operation that occurred - โœ… **Audit Trail**: You can see the sequence (0, 1, 2) of operations - โœ… **Analytics Ready**: Perfect for counting operations, tracking changes - โœ… **Integration Friendly**: Standard SQL database that other tools can access - โŒ **Less User-Friendly**: Shows operation metadata rather than final state --- **Key Differences:** - **Document Query**: Gets the current state directly from the document model - **Subgraph Query**: Gets processed/transformed data from your relational database - **Combined Power**: You can query both in a single GraphQL request for rich UIs This demonstrates how the supergraph provides a unified interface to both your document models and your custom subgraphs, allowing you to query and mutate data from the same endpoint.
## Use the Data in Frontend Applications **Integration Options** Your processed data can now be consumed by any GraphQL client: - **React**: Using Apollo Client, urql, or Relay - **Next.js**: API routes, getServerSideProps, or app router - **Mobile Apps**: React Native, Flutter, or native iOS/Android - **Desktop Apps**: Electron, Tauri, or other frameworks - **Third-party Tools**: Any tool that supports GraphQL APIs ### React Hooks **Coming Soon**: This section will cover how to use React hooks to consume your subgraph data in React applications. For now, you can use standard GraphQL clients like Apollo or urql to query your supergraph endpoint. ### Next.js API Route Example **Why API Routes?** Next.js API routes are useful when you need to: - Add server-side authentication or authorization - Transform data before sending to the client - Implement caching or rate limiting - Proxy requests to avoid CORS issues - Add logging or monitoring ```ts // pages/api/todos.ts export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { // Only allow GET requests for this endpoint if (req.method !== "GET") { return res.status(405).json({ message: "Method not allowed" }); } // Extract driveId from query parameters, default to "powerhouse" const { driveId = "powerhouse" } = req.query; try { // Query your subgraph or database directly // In production, you might want to add authentication headers here const response = await fetch("http://localhost:4001/graphql", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: ` query GetTodoList($driveId: String) { todoList(driveId: $driveId) { id name completed createdAt updatedAt } } `, variables: { driveId }, }), }); const data = await response.json(); // Return the todos array from the GraphQL response res.status(200).json(data.data.todoList); } catch (error) { // Log the error for debugging (in production, use proper logging) console.error("Failed to fetch todos:", error); // Return a generic error message to the client res.status(500).json({ error: "Failed to fetch todos" }); } } ``` ## Summary You've successfully created a relational database processor that: 1. โœ… **Listens for document changes** - Automatically detects when todo documents are modified 2. โœ… **Stores data in a structured database** - Transforms document operations into relational data 3. โœ… **Provides type-safe database operations** - Uses TypeScript for compile-time safety 4. โœ… **Exposes data through GraphQL** - Makes processed data available via a unified API 5. โœ… **Can be consumed by frontend applications** - Ready for integration with any GraphQL client This processor will automatically sync your document changes to the relational database, making the data available for complex queries, reporting, and integration with other systems. **Real-World Applications:** This pattern is commonly used for: - **Analytics dashboards** showing document usage patterns - **Business intelligence** reports on document data - **Integration** with existing enterprise systems - **Search and filtering** with complex SQL queries - **Data archival** and compliance requirements --- ## Publish your package > Source: https://powerhouse.academy/academy/MasteryTrack/Launch/PublishYourProject **WARNING:** **This guide assumes familiarity with building document models in Vetra Studio.** Please start with the [**Get Started**](/) chapter or [**Document Model Creation**](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema) section if you are new to building document models. This guide covers the process of **building** and **publishing** a Powerhouse package to NPM. **INFO:** - **Powerhouse Package**: A collection of modules published to NPM that can be installed on a server instance or locally. Organizations build packages for specific purposes or workflows. - **Powerhouse Modules**: The building blocks of your packageโ€”document models, editors, processors, or scripts. - **Vetra Studio**: The development hub where you assemble specifications for your package. Each module is defined through specification documents that drive code generation. - **Powerhouse Drive-apps**: Customized drive interfaces that enhance document functionality and workflows within a drive. ![Key Concepts](images/keyconcepts.png) ## 1. Building your project ### 1.1. Initialize your project Start by initializing a new Powerhouse project: ```bash ph init ``` You'll be prompted to name your project, which will become the package name when published to NPM.
Command not working? Did you install `ph-cmd`? The Powerhouse CLI (`ph-cmd`) is a command-line interface tool that provides essential commands for managing Powerhouse projects. Install it globally using: ```bash pnpm install -g ph-cmd ``` Key commands include: - `ph vetra --watch` for launching Vetra Studio - `ph connect` for running Connect locally (alternative) - `ph switchboard` or `ph reactor` for starting the API service - `ph init` to start a new project - `ph help` to get an overview of all available commands
How to use different branches? When using the Powerhouse CLI, you can access dev & staging branches for experimental features or bugfixes under development. | Command | Description | | ---------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** | Install latest stable version | | **pnpm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
### 1.2. Launch Vetra Studio Launch Vetra Studio in interactive watch mode for development: ```bash ph vetra --interactive --watch ``` This mode provides: - **Interactive confirmations** before code generation - **Dynamic reloading** for document-models and editors - **Live preview** of your changes in Vetra Studio
Alternatively: Use Connect You can also use Connect for local development: ```bash ph connect ``` This opens Connect in your browser at `http://localhost:3000/`, providing a local Reactor for testing.
### 1.3. Configure package details In Vetra Studio, navigate to the **Package Details** section of your Vetra Studio drive to configure: - **Package name**: Use the format `@your-org-ph/package-name` (the `-ph` suffix identifies Powerhouse ecosystem packages) - **Description**: A meaningful description of your package - **Keywords**: Search terms for discoverability - **Version**: Following semantic versioning (e.g., `1.0.0`) - **Author** and **License** information These details are stored in your `package.json` and a `manifest.json` file is automatically generated from this configuration. ```json { "name": "@your-org-ph/package-name", "version": "1.0.0", "author": "Your Name", "license": "AGPL-3.0-only", "publishConfig": { "access": "public" } } ``` ### 1.4. Build your modules With Vetra Studio running, build your package modules: 1. **Document Models**: Define your document model specifications in Vetra Studio. The state schema and operations are automatically generated into code. 2. **Editors**: Create editor specifications linked to your document models. Vetra generates the scaffolding code. 3. **Reducers**: Implement the state transition logic in the generated reducer files. 4. **Unit Tests**: Write tests for your reducer logic in the `tests/` directory. Run tests to verify your implementation: ```bash pnpm run test ``` ### 1.5. Verify your build Build your project to verify everything compiles correctly: ```bash pnpm build ``` This creates an optimized build output ready for distribution. Test the build locally: ```bash ph vetra --interactive --watch ``` Create documents of your defined types and verify all functionality works as expected. ### 1.6. Store in version control Initialize a git repository to track changes: ```bash git init git add . git commit -m "Initial commit" ``` ## 2. Publishing your project ### 2.1. Set up NPM organization Create an organization on [NPM](https://www.npmjs.com/) using the naming convention `@yourorganization-ph`: - The `-ph` suffix identifies Powerhouse ecosystem packages - Example: `@acme-ph` We recommend using a **dedicated NPM account** for your organization rather than a personal account. ### 2.2. Log in to NPM ```bash npm login ``` Follow the prompts in your terminal or browser to authenticate. ### 2.3. Version your package Use semantic versioning to update your package version. The `pnpm version` command will: - Update the `version` in `package.json` - Create a Git commit for the version change - Create a Git tag (e.g., `v1.0.1`) ```bash # Patch release (1.0.0 โ†’ 1.0.1) - bugfixes pnpm version patch # Minor release (1.0.1 โ†’ 1.1.0) - new features pnpm version minor # Major release (1.1.0 โ†’ 2.0.0) - breaking changes pnpm version major ``` ### 2.4. Push to Git Push your commits and version tag to your remote repository: ```bash git push origin main git push origin vX.Y.Z # Replace with your actual tag ``` ### 2.5. Publish to NPM ```bash pnpm publish ``` For scoped packages intended to be public, ensure your `package.json` includes the `publishConfig` shown earlier, or use: ```bash pnpm publish --access public ``` To publish a pre-release version: ```bash # Ensure your version reflects the pre-release (e.g., 1.1.0-beta.0) pnpm publish --tag beta ``` **INFO:** - **Git Tags**: Markers in your repository history (e.g., `v1.0.0`) created by `pnpm version` - **NPM Dist-Tags**: Labels pointing to published versions (`latest`, `beta`, `next`). When publishing without a tag, the version gets the `latest` tag by default. ::: ## 3. Installing your package Once published, install your package in any Powerhouse environment: ```bash ph install @your-org-ph/package-name ``` This makes your document models and editors available within that Powerhouse instance. ### Package Manager UI A visual package manager can be found in Connect's settings (bottom left settings wheel), allowing installation at the click of a button. ![package manager](images/package-installer.png) --- ## 4. Publishing to the Vetra Registry (v6.0.0-dev.153+) **INFO:** Starting with `ph-cmd` version **6.0.0-dev.153**, packages can be published to the Vetra package registry using `ph publish`. This enables **dynamic package loading** โ€” Connect can detect the required package from a document file and prompt the user to install it directly from the registry. This flow applies to **newly created projects** initialized with v6.0.0-dev.153 or later. Existing projects will need to be migrated. ### 4.1. Set up your environment Install or update the Powerhouse CLI: ```bash pnpm install -g ph-cmd@6.0.0-dev.153 ``` Create a new project pinned to this version: ```bash ph init --pnpm --version=6.0.0-dev.153 your-test-project ``` Start Vetra in watch mode: ```bash cd your-test-project && ph vetra --watch ``` ### 4.2. Build your package 1. Create a document model and editor in Vetra Studio. Using Claude is recommended for faster scaffolding. 2. Create an example document in Connect and export it โ€” you will use this to test dynamic loading later. ### 4.3. Configure package metadata (first publish only) Before publishing for the first time, update two files: - **`powerhouse.manifest.json`** โ€” set `name`, `description`, and `publisher` - **`package.json`** โ€” update the `name` field to match Then create an account on the Vetra dev registry: ```bash npm adduser --registry=https://registry.dev.vetra.io ``` ### 4.4. Publish ```bash ph publish --registry=https://registry.dev.vetra.io ``` Your package will be available at: - Registry index: `https://registry.dev.vetra.io/` - Package page: `https://registry.dev.vetra.io/your-package-name` **TIP:** Bump the `version` field in `package.json` before every subsequent publish. ### 4.5. Test dynamic loading in Connect 1. Open your Connect instance on [staging.vetra.io](https://staging.vetra.io) (or use `https://connect.sharp-dove-96.vetra.io/`) 2. Create a local drive 3. Drag and drop the exported document into the drive 4. Connect will detect the required package and prompt you to install it from the registry 5. Once installed, open the document with your editor --- **Congratulations on publishing your package!** Your document models and editors are now available for installation across the Powerhouse ecosystem. --- ## Setup your environment > Source: https://powerhouse.academy/academy/MasteryTrack/Launch/SetupEnvironment ## Introduction Powerhouse is a powerful platform that helps you manage and deploy your applications efficiently. This guide will walk you through the process of setting up both the Powerhouse CLI and configuring your server machine to run Powerhouse services. Whether you're setting up a development environment or preparing for production deployment, this guide provides all the necessary steps and considerations. **TIP:** This guide covers **VM/server-based deployment** with direct installation. If you prefer **containerized deployment**, see the [Docker Deployment Guide](../docs/docs/05-DockerDeployment.md). **Choose Docker if:** You want the fastest path to production, prefer containerized workflows, or are deploying to cloud platforms. **Choose Direct Installation if:** You need maximum performance, want full control, or are setting up a dedicated server. ## Prerequisites Before you begin, ensure you have a Linux-based system (Ubuntu or Debian recommended), sudo privileges, and a stable internet connection. These are essential for the installation and configuration process. The system should have at least 1GB of RAM and 10GB of free disk space for optimal performance. While these are minimum requirements, more resources will provide better performance, especially when running multiple services. Also make sure you have your preferred domain registered and created subdomains for your Connect & Switchboard instances.
**Setting up a Droplet (Digital Ocean) instance and connecting your domain** This tutorial will guide you through the process of creating a new virtual private server (called a "Droplet") on DigitalOcean and then pointing your custom domain name to it. This will allow users to access your server using a memorable URL like `www.yourdomain.com`. **Current Date:** May 15, 2024 ## Part 1: Setting up your DigitalOcean droplet A Droplet is a scalable virtual machine that you can configure to host your websites, applications, or other services. ### Step 1: Sign up or log in to DigitalOcean - If you don't have an account, go to [digitalocean.com](https://digitalocean.com) and sign up. You'll likely need to provide payment information. - If you already have an account, log in. ### Step 2: Create a new droplet 1. From your DigitalOcean dashboard, click the green "Create" button in the top right corner and select "Droplets". 2. **Choose an Image:** - **Distributions:** Select a base Linux distribution like Ubuntu (a popular choice, e.g., Ubuntu 22.04 LTS), Fedora, Debian, etc. - **Marketplace:** You can also choose from pre-configured 1-Click Apps (e.g., WordPress, Docker, LAMP stack). This can save you setup time. For this general tutorial, we'll assume a base distribution. 3. **Choose a Plan (Size):** - **Shared CPU:** Good for smaller projects, development, or low-traffic sites. Options like "Basic" Droplets fall here. - **Dedicated CPU:** For production applications needing consistent performance (General Purpose, CPU-Optimized, Memory-Optimized, Storage-Optimized Droplets). - Start with a basic plan that fits your budget and expected needs; you can resize your Droplet later if necessary. 4. **Choose a Datacenter Region:** - Select a server location closest to your target audience to minimize latency. - For example, if your users are primarily in Europe, choose a European datacenter like Amsterdam, Frankfurt, or London. 5. **Authentication:** - **SSH Keys (Recommended for security):** If you have an SSH key pair, you can add your public key. This is more secure than using passwords. Click "New SSH Key" and paste your public key if it's not already added. - **Password:** If you choose this, create a strong root password. You'll use this to log in via SSH initially. 6. **Additional Options (Customize as needed):** - **VPC Network:** By default, your Droplet will be in your default VPC for the chosen region. You can change this if you have custom networking setups. - **Monitoring:** A free metrics monitoring service. It's a good idea to enable this. - **User Data:** Allows you to run initial configuration scripts when the Droplet is first created. - **Backups (Recommended for production):** Enable automated weekly backups for a small additional fee. - **Volume (Additional Storage):** Attach block storage if you need more disk space than the Droplet plan offers. 7. **Finalize and Create:** - Choose a Hostname: Give your Droplet a name (e.g., `my-web-server`). This is for your reference within DigitalOcean. - Add Tags (Optional): Organize your resources with tags. - Select Project: Assign the Droplet to a project. - Review your selections and click the "Create Droplet" button at the bottom. ### Step 3: Access your droplet It will take a minute or two for your Droplet to be provisioned. Once it's ready, its IP address will be displayed in your Droplets list. To log in via SSH: 1. Open a terminal (on macOS/Linux) or an SSH client like PuTTY (on Windows). You can also use Digital Ocean's web 'Console'. 2. Use one of these commands: ```bash # If using password authentication ssh root@YOUR_DROPLET_IP # If using SSH key for a specific user ssh your_user@YOUR_DROPLET_IP # If using a specific SSH key ssh -i /path/to/your/private_key root@YOUR_DROPLET_IP ``` 3. If you used a password, you'll be prompted to enter it. 4. If it's your first time logging in, you might be asked to change the root password. Now your Droplet is running! Now you can continue with the Powerhouse tutorial or any next steps. ### DNS configuration #### Option A: Using DigitalOcean's nameservers (recommended) 1. **Add Your Domain to DigitalOcean:** - Go to "Networking" โ†’ "Domains" - Enter your domain name (e.g., `yourdomain.com`) - Click "Add Domain" 2. **Start with Updating Nameservers at Your Domain Registrar:** - Log in to your domain registrar - Update nameservers to: ``` ns1.digitalocean.com ns2.digitalocean.com ns3.digitalocean.com ``` 3. **Create DNS Records:** - **Root Domain (A Record):** - **TYPE:** A - **HOSTNAME:** @ - **WILL DIRECT TO:** Your Droplet's IP - **TTL:** 3600 - **WWW Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** www - **WILL DIRECT TO:** Your Droplet's IP - **TTL:** 3600 - **Connect Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** connect - **WILL DIRECT TO:** Your Droplet's IP - **TTL:** 3600 - **Switchboard Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** switchboard - **WILL DIRECT TO:** Your Droplet's IP - **TTL:** 3600 #### Option B: Using your existing nameservers (NS locked) 1. **Just Create DNS Records at Your Registrar:** - **Root Domain (A Record):** - **TYPE:** A - **HOSTNAME:** @ - **VALUE:** Your Droplet's IP - **TTL:** 3600 - **WWW Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** www - **VALUE:** Your Droplet's IP - **TTL:** 3600 - **Connect Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** connect - **VALUE:** Your Droplet's IP - **TTL:** 3600 - **Switchboard Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** switchboard - **VALUE:** Your Droplet's IP - **TTL:** 3600 **Note:** DNS changes may take up to 48 hours to propagate globally. ### Verify configuration 1. Use DNS lookup tools to verify your records: ```bash dig +short yourdomain.com dig +short www.yourdomain.com dig +short connect.yourdomain.com dig +short switchboard.yourdomain.com ``` 2. All should return your Droplet's IP address **Congratulations!** You have successfully set up your DigitalOcean Droplet and configured your domain. Your server is now ready to host your Powerhouse services.
**Setting up an EC2 instance and connecting your domain** This tutorial will guide you through the process of assigning a static IP (Elastic IP) to your EC2 instance and configuring your domain to point to it. **Current Date:** May 15, 2024 - Make sure your region is set to eu-west-1 (Ireland) - Name your instance something like `cloud-server` or your project's name - Select Ubuntu 24.04 LTS - Architecture 64-bit (x86) - Scroll down to Instance type and select t2.medium (recommended) - 2 vCPUs and 4 GiB of memory are the recommended minimum specs - For larger projects or higher load, consider t2.large or t2.xlarge - Create a new key pair and save it in a secure location from which you can connect to your instance with the SSH client later. - Configure the security group to allow inbound traffic: - SSH (Port 22) from your IP address - HTTP (Port 80) from anywhere - HTTPS (Port 443) from anywhere - Custom TCP (Port 8442) for Connect - Custom TCP (Port 8441) for Switchboard - **Launch the instance** **WARNING:** Make sure to keep your key pair file (.pem) secure and never share it. Without it, you won't be able to access your instance. Also, consider setting up AWS IAM roles and policies for better security management. ## Part 1: Assigning a static IP to EC2 instance ### Step 1: Allocate elastic IP 1. Navigate to the EC2 service in the AWS console 2. Choose "Elastic IPs" from the navigation pane 3. Select "Allocate new address" 4. Select the VPC where your EC2 instance is located 5. Click "Allocate" ### Step 2: Associate elastic IP 1. Go back to the EC2 console and select your instance 2. From the "Networking" tab, expand "Network interfaces" 3. Note the "Interface ID" of the network interface 4. Select the "Interface ID" to manage its IP addresses 5. Choose "Actions", then "Manage IP Addresses" 6. Find the Elastic IP you allocated and click "Associate" ## Part 2: DNS configuration ### Option A: Using AWS Route 53 (recommended) 1. **Add Your Domain to Route 53:** - Go to Route 53 โ†’ "Hosted zones" - Click "Create hosted zone" - Enter your domain name (e.g., `yourdomain.com`) - Click "Create" 2. **Update Nameservers at Your Domain Registrar:** - Log in to your domain registrar - Update nameservers to the ones provided by Route 53 - They will look like: ``` ns-1234.awsdns-12.org ns-567.awsdns-34.com ns-890.awsdns-56.net ns-1234.awsdns-78.co.uk ``` 3. **Create DNS Records:** - **Root Domain (A Record):** - **TYPE:** A - **HOSTNAME:** @ - **VALUE:** Your Elastic IP - **TTL:** 3600 - **WWW Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** www - **VALUE:** Your Elastic IP - **TTL:** 3600 - **Connect Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** connect - **VALUE:** Your Elastic IP - **TTL:** 3600 - **Switchboard Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** switchboard - **VALUE:** Your Elastic IP - **TTL:** 3600 ### Option B: Using your existing nameservers 1. **Create DNS Records at Your Registrar:** - **Root Domain (A Record):** - **TYPE:** A - **HOSTNAME:** @ - **VALUE:** Your Elastic IP - **TTL:** 3600 - **WWW Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** www - **VALUE:** Your Elastic IP - **TTL:** 3600 - **Connect Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** connect - **VALUE:** Your Elastic IP - **TTL:** 3600 - **Switchboard Subdomain (A Record):** - **TYPE:** A - **HOSTNAME:** switchboard - **VALUE:** Your Elastic IP - **TTL:** 3600 1. **Set Up DNS First:** - Create A records for all subdomains before running the setup script - Point them to your EC2 instance's public IP address - Wait for DNS propagation before requesting SSL certificates ### Verify configuration 1. Use DNS lookup tools to verify your records: ```bash dig +short yourdomain.com dig +short www.yourdomain.com dig +short connect.yourdomain.com dig +short switchboard.yourdomain.com ``` 2. All should return your Elastic IP address **Congratulations!** You have successfully set up your EC2 instance with a static IP and configured your domain. Your server is now ready to host your Powerhouse services.
## 1. Setting up a new cloud environment The `install` script provides a streamlined way to install the Powerhouse CLI tool and all its necessary dependencies. This script handles the installation of Node.js 24, pnpm, and the Powerhouse CLI itself. It's designed to work across different Linux distributions, though it's optimized for Ubuntu and Debian-based systems. It also prepares your machine for running Powerhouse services. It handles everything from package installation to service configuration, making the setup process straightforward and automated. This script is particularly useful for setting up new servers or reconfiguring existing ones. ### Installation 1. Run the setup script: ```bash curl -fsSL https://apps.powerhouse.io/install | bash # for macOS, Linux, and WSL ``` 2. After installation, source your shell configuration: ```bash source ~/.bashrc # or source ~/.zshrc if using zsh ``` 3. Verify that the Powerhouse CLI is ready to be installed in the next step: ```bash ph --version ``` You will see that `ph-cli` is not yet installed. This is expected, as it will be installed by the service setup command. 4. Create a project with `ph-init `. 5. After creation, move into the project with `cd `. Up next is the configurations of your services. ### Service configuration Next, run ```bash ph service setup ``` Follow the interactive prompts. This command installs the Powerhouse services (Connect and Switchboard) and guides you through their configuration. **INFO:** The script takes care of all the necessary service configuration automatically. It installs and configures **Nginx** as a reverse proxy, sets up SSL certificates, and configures the proxy settings for optimal performance. It also installs **PM2** for process management and starts your services with the appropriate configuration based on your SSL choice. The Nginx configuration includes optimizations for **WebSocket connections**, static file serving, and security headers. PM2 is configured to automatically restart services if they crash and to start them on system boot. The setup command will prompt you for the following information: #### Package installation During this phase, you can enter package names that you want to install. For example, you might want to `ph install @powerhousedao/todo-demo-package` or other Powerhouse packages. This step is crucial for adding the specific functionality you need. You can also press Enter to skip this step and install packages later using the `ph install` command. #### Database configuration The script offers two options for database configuration: - **Option 1: Local Database** Sets up a local PostgreSQL database, which is ideal for development or small deployments. It automatically creates a database user with a secure random password and configures the database to accept local connections. This option is perfect for getting started quickly. - **Option 2: Remote Database** Allows you to connect to a remote PostgreSQL database by providing a connection URL in the format `postgres://user:password@host:port/db`. This is recommended for production environments. #### SSL configuration For SSL configuration, you have two choices: - **Option 1: Let's Encrypt (Recommended for Production)** This option requires you to provide a base domain (e.g., `powerhouse.xyz`) and subdomains for your services. The script will automatically obtain and configure SSL certificates for your domains. - **Option 2: Self-signed Certificate** This is suitable for development or testing. It uses your machine's hostname and generates a self-signed certificate. Browsers will show security warnings with this option. #### Domain setup You will be asked to enter your `connect` and `switchboard` subdomains to complete the setup. If you need more information, revisit the cloud provider setup sections at the beginning of this guide. #### Security features Security is a top priority. The script implements automatic SSL certificate management, generates secure database passwords, and configures security headers in Nginx, and sets up proper proxy settings to support WebSocket connections securely. ## 2. Verifying the setup After the installation is complete, it's important to verify that everything is working correctly. You can check the status of your services using PM2, verify the Nginx configuration, and ensure your SSL certificates are properly installed. This step is crucial for identifying any potential issues before they affect your users. 1. Check service status of switchboard and connect: ```bash ph service status ``` You can also use ```bash ph service start | stop | restart ``` to start | stop | restart switchboard and connect 2. View Nginx configuration: ```bash sudo nginx -t ``` 3. Check SSL certificates: ```bash sudo certbot certificates # if using Let's Encrypt ``` ## 3. Accessing the services Once everything is set up, you can access your services through the configured domains. If you chose Let's Encrypt, your services will be available at their respective subdomains. With a self-signed certificate, you'll access the services through your machine's hostname with the appropriate base paths. The services are configured to use HTTPS by default, ensuring secure communication. ### With Let's Encrypt - Connect: `https://connect.yourdomain.com` - Switchboard: `https://switchboard.yourdomain.com` ### With self-signed certificate - Connect: `https://your-hostname/connect` - Switchboard: `https://your-hostname/switchboard` ## 4. Troubleshooting When issues arise, there are several common problems you might encounter. - The "`ph`: command not found" error usually means you need to source your shell configuration file. - Nginx configuration errors can be investigated through the error logs, and service issues can be diagnosed using PM2 logs. - SSL certificate problems often relate to DNS settings or certificate paths. Understanding these common issues and their solutions will help you maintain a stable Powerhouse installation. ### Common issues 1. **"`ph`: command not found"** - Run `source ~/.bashrc` or restart your terminal - Verify that the `PNPM_HOME` environment variable is set correctly - Check if the `ph` binary exists in the `PNPM_HOME` directory 2. **Nginx configuration errors** - Check logs: `sudo tail -f /var/log/nginx/error.log` - Verify that all required modules are installed - Ensure that the SSL certificate paths are correct 3. **Service not starting** - Check PM2 logs: `pm2 logs` - Verify that the service ports are not in use - Check if the service has the required permissions 4. **SSL certificate issues** - Verify domain DNS settings - Check certificate paths in Nginx config - Ensure that the certificate files are readable by Nginx ## 5. Maintenance Regular maintenance is crucial for keeping your Powerhouse installation running smoothly. You can update services using the Powerhouse CLI, restart services through PM2, and monitor logs to ensure everything is functioning correctly. Regular maintenance helps prevent issues and ensures that your services are running with the latest security patches and features. ### Updating services ```bash ph update ``` ### Restarting services ```bash ph service restart ``` ### Checking service status and logs ```bash ph service status ``` ## 6. Security notes Maintaining security is an ongoing process. It's essential to keep your database credentials secure and regularly update your SSL certificates. Regular monitoring of system logs helps identify potential security issues, and keeping your system and packages updated ensures you have the latest security patches. Consider implementing additional security measures such as firewall rules, intrusion detection systems, and regular security audits. ## 7. Backup Regular backups are crucial for data safety. The database can be backed up using pg_dump, and your configuration files can be archived using tar. These backups should be stored securely and tested regularly to ensure they can be restored if needed. Consider implementing an automated backup schedule and storing backups in multiple locations for redundancy. ### Database backup ```bash pg_dump -U powerhouse -d powerhouse > backup.sql ``` ### Configuration backup ```bash sudo tar -czf powerhouse-config.tar.gz /etc/powerhouse/ ``` ## 8. Best practices To get the most out of your Powerhouse installation, follow these best practices: 1. **Regular Updates**: Keep your system, packages, and services updated to the latest stable versions. 2. **Monitoring**: Set up monitoring for your services to detect issues early. 3. **Documentation**: Keep documentation of your configuration and any customizations. 4. **Testing**: Test your backup and restore procedures regularly. 5. **Security**: Regularly review and update your security measures. ## 9. Getting help If you encounter issues or need assistance, there are several resources available: 1. **Documentation**: Check the official Powerhouse documentation for detailed information. 2. **Community**: Join the Powerhouse community forums or chat channels. 3. **Support**: Contact Powerhouse support for professional assistance. 4. **GitHub**: Report issues or contribute to the project on GitHub. --- ## Configure your environment > Source: https://powerhouse.academy/academy/MasteryTrack/Launch/ConfigureEnvironment After successfully setting up your server and installing the Powerhouse services using the `ph service setup` command as described in the [Setup Environment](../docs/docs/03-SetupEnvironment.md) guide, the next crucial step is to configure your environment. Proper configuration ensures that your Powerhouse Connect and Switchboard instances behave exactly as you need them to for your specific application. Powerhouse offers two primary methods for configuration: 1. **Environment Variables**: Using a `.env` file in your project root. This is a straightforward way to set configuration values. 2. **Configuration Files**: For more complex configurations, some services might use a JSON configuration file (e.g., `powerhouse.config.json`). **WARNING:** A key principle to remember is that **environment variables will always override values set in configuration files**. This allows for flexible setups, where you can have default configurations in a file and override them for different environments (development, staging, production) using environment variables. This guide will walk you through both methods and provide details on common configuration options, including setting up authorization. ## Using environment variables The most common way to configure Powerhouse services is through environment variables. You can place these variables in a `.env` file at the root of your project directory. When you run `ph service start` or `ph service restart`, these variables are loaded into the environment of your running services. ### How to create and edit your .env file If you're on your cloud server, you can create and edit the `.env` file directly: 1. Navigate to your project directory: `cd ` 2. Open the `.env` file with a text editor like `vim` or `nano`: ```bash vim .env ``` 3. Add your configuration variables, one per line, in the `KEY="VALUE"` format. ```env # Example for Connect PH_CONNECT_STUDIO_MODE="true" PH_CONNECT_DISABLE_ADD_DRIVE="true" ``` 4. Save the file and exit the editor (in `vim`, press `Esc`, then type `:wq` and press `Enter`). 5. For the changes to take effect, you must restart the Powerhouse services: ```bash ph service restart ``` ### Common environment variables for Connect The Powerhouse Connect application has a wide range of available environment variables to toggle features and change its behavior. Below is a list of some variables you can configure. ```bash # build arguments BASE_PATH="/" # vite base path BASE_HREF="./" # electron-forge base href PH_CONNECT_APP_REQUIRES_HARD_REFRESH="true" SENTRY_AUTH_TOKEN="" SENTRY_ORG="" SENTRY_PROJECT="" # environment variables LOG_LEVEL="info" ## app configuration & feature flags PH_CONNECT_DISABLE_ADD_DRIVE="false" PH_CONNECT_WARN_OUTDATED_APP="false" PH_CONNECT_STUDIO_MODE="false" PH_CONNECT_ROUTER_BASENAME="/" PH_CONNECT_DEFAULT_DRIVES_URL="" PH_CONNECT_ENABLED_EDITORS="" PH_CONNECT_DISABLE_ADD_PUBLIC_DRIVES="false" PH_CONNECT_SEARCH_BAR_ENABLED="false" PH_CONNECT_DISABLE_ADD_CLOUD_DRIVES="false" PH_CONNECT_DISABLE_ADD_LOCAL_DRIVES="false" PH_CONNECT_DISABLE_DELETE_PUBLIC_DRIVES="false" PH_CONNECT_DISABLE_DELETE_CLOUD_DRIVES="false" PH_CONNECT_DISABLE_DELETE_LOCAL_DRIVES="false" PH_CONNECT_PUBLIC_DRIVES_ENABLED="true" PH_CONNECT_CLOUD_DRIVES_ENABLED="true" PH_CONNECT_LOCAL_DRIVES_ENABLED="true" PH_CONNECT_ARBITRUM_ALLOW_LIST="" PH_CONNECT_RWA_ALLOW_LIST="" PH_CONNECT_HIDE_DOCUMENT_MODEL_SELECTION_SETTINGS="true" PH_CONNECT_RENOWN_URL="https://www.renown.id" PH_CONNECT_RENOWN_NETWORK_ID="eip155" PH_CONNECT_RENOWN_CHAIN_ID=1 PH_CONNECT_DISABLED_EDITORS="powerhouse/document-drive" PH_CONNECT_ANALYTICS_DATABASE_NAME="" PH_CONNECT_ANALYTICS_DATABASE_WORKER_DISABLED="false" ## error tracking PH_CONNECT_SENTRY_DSN="" PH_CONNECT_SENTRY_PROJECT="" PH_CONNECT_SENTRY_ENV="prod" PH_CONNECT_SENTRY_TRACING_ENABLED="false" ## analytics PH_CONNECT_GA_TRACKING_ID= FILE_UPLOAD_OPERATIONS_CHUNK_SIZE="50" PH_CONNECT_VERSION_CHECK_INTERVAL="3600000" PH_CONNECT_CLI_VERSION="" ## set during build APP_VERSION="" SENTRY_RELEASE="" ``` You can find the most up-to-date list of variables in the source repository: [https://github.com/powerhouse-inc/powerhouse/blob/main/apps/connect/.env](https://github.com/powerhouse-inc/powerhouse/blob/main/apps/connect/.env) ## Using a configuration file For services like the Switchboard, you can also use a `powerhouse.config.json` file for more structured configuration, especially for features like authorization. ### Configuring authorization A critical aspect of your environment configuration is setting up authorization to control who can access your services and what they can do. As detailed in our dedicated [Switchboard Authorization](/academy/MasteryTrack/BuildingUserExperiences/Authorization/Authorization) guide, you can manage access using authentication, supreme admin access, and document protection. Here's a quick overview of how you can configure authorization: #### Via environment variables You can set the authorization config directly in your `.env` file. ```bash # Required: Enable/disable authentication AUTH_ENABLED=true # Optional: Comma-separated list of admin wallet addresses (full access, bypasses all checks) ADMINS="0x123...,0x456..." # Optional: Make all new documents protected by default (requires explicit grants) DEFAULT_PROTECTION=true # Optional: Enable per-document permission management DOCUMENT_PERMISSIONS_ENABLED=true ``` #### Via `powerhouse.config.json` For a cleaner setup, you can define authorization in `powerhouse.config.json` in your project root. ```json { "switchboard": { "auth": { "enabled": true, "admins": ["0x123...", "0x456..."] } } } ``` Remember, if you define `AUTH_ENABLED=false` as an environment variable, it will override the `enabled: true` setting in your JSON file. For a complete understanding of how authorization (authentication, admin access, and document protection) works, please refer to the full [Authorization guide](/academy/MasteryTrack/BuildingUserExperiences/Authorization/Authorization). ## Applying your changes Regardless of which method you use to update your configuration, the changes will not be applied until you restart your services. Use the following command to do so: ```bash ph service restart ``` This will stop and then start the Connect and Switchboard services, ensuring they load the new configuration. You can check the status with `ph service status`. ## Summary Configuring your environment is a key step to tailor the Powerhouse platform to your needs. By using a combination of `.env` files for simple key-value settings and `powerhouse.config.json` for more structured data, you have fine-grained control over all features, from the UI of the Connect app to the security and authorization on your Switchboard. --- ## Docker deployment guide > Source: https://powerhouse.academy/academy/MasteryTrack/Launch/DockerDeployment ## Introduction Powerhouse provides official Docker images for deploying your applications in containerized environments. This guide covers the available Docker images, how to use them with Docker Compose, and the environment variables you can configure. Docker deployment is ideal for: - **Production environments** that require consistent, reproducible deployments - **Development teams** that want to share a common environment - **CI/CD pipelines** that need automated testing and deployment - **Cloud platforms** like AWS ECS, Google Cloud Run, or Kubernetes **TIP:** This guide covers **Docker-based deployment**. If you prefer **traditional VM/server deployment** with direct installation, see the [Setup Environment Guide](../docs/docs/03-SetupEnvironment.md). **Choose Docker if:** You want the fastest path to production, prefer containerized workflows, or are deploying to cloud platforms. **Choose Direct Installation if:** You need maximum performance, want full control, or are setting up a dedicated server. ## Available Docker Images Powerhouse publishes three official Docker images to the GitHub Container Registry (ghcr.io): ### 1. Connect The Connect image provides the Powerhouse web application frontend with an embedded Nginx server. ``` ghcr.io/powerhouse-inc/powerhouse/connect ``` **Available tags:** - `latest` - Latest stable release - `dev` - Development builds - `staging` - Staging builds - `vX.Y.Z` - Specific version tags (e.g., `v1.0.0`) ### 2. Switchboard The Switchboard image provides the backend API server that handles document synchronization and GraphQL endpoints. ``` ghcr.io/powerhouse-inc/powerhouse/switchboard ``` **Available tags:** - `latest` - Latest stable release - `dev` - Development builds - `staging` - Staging builds - `vX.Y.Z` - Specific version tags (e.g., `v1.0.0`) ### 3. Academy The Academy image provides the documentation website. ``` ghcr.io/powerhouse-inc/powerhouse/academy ``` **Available tags:** - `latest` - Latest stable release - `dev` - Development builds - `staging` - Staging builds - `vX.Y.Z` - Specific version tags (e.g., `v1.0.0`) ## Quick Start with Docker Compose The easiest way to run Powerhouse locally is using Docker Compose. Create a `docker-compose.yml` file or use the one provided in the repository: ```yaml name: powerhouse services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:dev environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres - BASE_PATH=/ ports: - "127.0.0.1:3000:4000" networks: - powerhouse_network depends_on: postgres: condition: service_healthy switchboard: image: ghcr.io/powerhouse-inc/powerhouse/switchboard:dev environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres ports: - "127.0.0.1:4000:4001" networks: - powerhouse_network depends_on: postgres: condition: service_healthy postgres: image: postgres:16.1 ports: - "127.0.0.1:5444:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres - POSTGRES_USER=postgres networks: - powerhouse_network healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 3 networks: powerhouse_network: name: powerhouse_network ``` ### Running the Stack Start all services: ```bash docker compose up -d ``` View logs: ```bash docker compose logs -f ``` Stop all services: ```bash docker compose down ``` After starting, you can access: - **Connect**: http://localhost:3000 - **Switchboard**: http://localhost:4000 ## Environment Variables ### Connect Environment Variables | Variable | Description | Default | | ----------------------- | --------------------------------------------------- | -------- | | `PORT` | Port the server listens on | `3001` | | `PH_CONNECT_BASE_PATH` | Base URL path for the application | `/` | | `PH_REGISTRY_PACKAGES` | Comma-separated list of packages to load at startup | `""` | | `PH_CONNECT_SENTRY_DSN` | Sentry DSN for error tracking | `""` | | `PH_CONNECT_SENTRY_ENV` | Sentry environment name | `""` | | `DATABASE_URL` | PostgreSQL connection string | Required | #### Feature Flags | Variable | Description | Default | | --------------------------------------------------- | ---------------------------------- | --------- | | `PH_CONNECT_DEFAULT_DRIVES_URL` | Default drives URL to load | `""` | | `PH_CONNECT_ENABLED_EDITORS` | Enabled editor types (`*` for all) | `"*"` | | `PH_CONNECT_DISABLED_EDITORS` | Disabled editor types | `""` | | `PH_CONNECT_PUBLIC_DRIVES_ENABLED` | Enable public drives | `"true"` | | `PH_CONNECT_CLOUD_DRIVES_ENABLED` | Enable cloud drives | `"true"` | | `PH_CONNECT_LOCAL_DRIVES_ENABLED` | Enable local drives | `"true"` | | `PH_CONNECT_SEARCH_BAR_ENABLED` | Enable search bar | `"false"` | | `PH_CONNECT_DISABLE_ADD_PUBLIC_DRIVES` | Disable adding public drives | `"false"` | | `PH_CONNECT_DISABLE_ADD_CLOUD_DRIVES` | Disable adding cloud drives | `"false"` | | `PH_CONNECT_DISABLE_ADD_LOCAL_DRIVES` | Disable adding local drives | `"false"` | | `PH_CONNECT_DISABLE_DELETE_PUBLIC_DRIVES` | Disable deleting public drives | `"false"` | | `PH_CONNECT_DISABLE_DELETE_CLOUD_DRIVES` | Disable deleting cloud drives | `"false"` | | `PH_CONNECT_DISABLE_DELETE_LOCAL_DRIVES` | Disable deleting local drives | `"false"` | | `PH_CONNECT_HIDE_DOCUMENT_MODEL_SELECTION_SETTINGS` | Hide document model selection | `"true"` | #### Renown Authentication | Variable | Description | Default | | ------------------------------ | --------------------------------- | -------------------------- | | `PH_CONNECT_RENOWN_URL` | Renown authentication service URL | `"https://auth.renown.id"` | | `PH_CONNECT_RENOWN_NETWORK_ID` | Renown network identifier | `"eip155"` | | `PH_CONNECT_RENOWN_CHAIN_ID` | Renown chain ID | `1` | ### Switchboard Environment Variables #### Core Configuration | Variable | Description | Default | | ----------------------------- | ------------------------------------------------------ | ---------- | | `PORT` | Port the server listens on | `3000` | | `PH_SWITCHBOARD_PORT` | Alias for PORT | `$PORT` | | `DATABASE_URL` | PostgreSQL or SQLite connection string | `"dev.db"` | | `PH_SWITCHBOARD_DATABASE_URL` | Alias for DATABASE_URL | `"dev.db"` | | `BASE_PATH` | Base URL path for the API | `"/"` | | `PH_PACKAGES` | Comma-separated list of packages to install at startup | `""` | #### Authentication | Variable | Description | Default | | ------------------------------ | ------------------------------------------------------------ | --------- | | `AUTH_ENABLED` | Enable authentication | `"false"` | | `ADMINS` | Comma-separated list of admin wallet addresses (full access) | `""` | | `DEFAULT_PROTECTION` | Make all new documents protected by default | `"false"` | | `DOCUMENT_PERMISSIONS_ENABLED` | Enable per-document permission management | `"false"` | #### Error Tracking & Monitoring | Variable | Description | Default | | -------------------------- | ------------------------------------------------------- | ------- | | `SENTRY_DSN` | Sentry DSN for error tracking | `""` | | `SENTRY_ENV` | Sentry environment name (e.g., "production", "staging") | `""` | | `PYROSCOPE_SERVER_ADDRESS` | Pyroscope server address for performance profiling | `""` | ## Installing Custom Packages Connect loads custom packages via the `PH_REGISTRY_PACKAGES` environment variable (written to a registry config at startup). Switchboard installs packages via `PH_PACKAGES`. ```yaml services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:dev environment: - PH_REGISTRY_PACKAGES=@powerhousedao/todo-demo-package,@powerhousedao/another-package ``` Packages are installed using the `ph install` command before the service starts. ## Image Architecture ### Connect Image The Connect image is based on Alpine Linux and includes: - Node.js and pnpm - Nginx with Brotli compression - The `ph-cmd` CLI tool - A pre-initialized Powerhouse project At startup, the entrypoint script: 1. Loads any packages specified in `PH_REGISTRY_PACKAGES` 2. Builds the Connect frontend with `ph connect build` 3. Configures and starts Nginx to serve the built files ### Switchboard Image The Switchboard image is based on Node.js 24 and includes: - pnpm package manager - The `ph-cmd` CLI tool - Prisma CLI for database migrations - A pre-initialized Powerhouse project At startup, the entrypoint script: 1. Installs any packages specified in `PH_PACKAGES` 2. Runs Prisma database migrations (if `PH_REACTOR_DATABASE_URL` is set) 3. Starts the Switchboard server via Node.js ## Production Considerations ### Using Specific Version Tags For production deployments, always use specific version tags instead of `latest` or `dev`: ```yaml services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:v1.0.0 switchboard: image: ghcr.io/powerhouse-inc/powerhouse/switchboard:v1.0.0 ``` ### Database Persistence For production, ensure your PostgreSQL data is persisted using volumes: ```yaml services: postgres: image: postgres:16.1 volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=your-secure-password - POSTGRES_DB=powerhouse - POSTGRES_USER=powerhouse volumes: postgres_data: ``` ### Health Checks The provided docker-compose.yml includes health checks for PostgreSQL. Services wait for the database to be healthy before starting, preventing connection errors during startup. ### Network Security The example configuration binds ports to `127.0.0.1` only: ```yaml ports: - "127.0.0.1:3000:4000" ``` This prevents direct external access. In production, use a reverse proxy (like Nginx or Traefik) to: - Terminate SSL/TLS - Handle load balancing - Provide additional security headers ### Environment File For better security, use a `.env` file instead of hardcoding credentials: ```bash # .env POSTGRES_PASSWORD=your-secure-password DATABASE_URL=postgres://powerhouse:your-secure-password@postgres:5432/powerhouse ``` ```yaml services: switchboard: image: ghcr.io/powerhouse-inc/powerhouse/switchboard:latest env_file: - .env ``` ## Troubleshooting ### Container Won't Start Check the logs for errors: ```bash docker compose logs connect docker compose logs switchboard ``` ### Database Connection Issues Ensure the database is ready before services start: ```bash docker compose logs postgres ``` Verify the `DATABASE_URL` format: ``` postgres://user:password@host:port/database ``` ### Package Installation Fails If custom packages fail to install, check: 1. Package name is correct 2. Network connectivity from container 3. Container has access to npm registry ### Permission Issues If you encounter permission issues with volumes: ```bash # Fix ownership sudo chown -R 1000:1000 ./data ``` ## Building Custom Images You can extend the official images for custom deployments: ```dockerfile FROM ghcr.io/powerhouse-inc/powerhouse/connect:latest # Install additional packages at build time RUN ph install @powerhousedao/my-custom-package # Add custom configuration COPY my-nginx.conf /etc/nginx/nginx.conf.template ``` Build and push your custom image: ```bash docker build -t my-registry/my-connect:latest . docker push my-registry/my-connect:latest ``` ## Next Steps - Learn about [Environment Configuration](../docs/docs/04-ConfigureEnvironment.md) for more detailed setup options - Explore [Publishing Your Project](../docs/docs/02-PublishYourProject.md) to create your own packages - Check the [Setup Environment Guide](../docs/docs/03-SetupEnvironment.md) for VM-based deployments --- # Example Use Cases ## Example use-cases > Source: https://powerhouse.academy/academy/ExampleUsecases/Overview Explore real-world examples of applications built with the Powerhouse ecosystem. Each use-case provides a complete walkthrough from project setup to a fully functional application.

Chatroom

Build a real-time chat application with message posting and emoji reactions. Learn document models, reducers, and real-time synchronization.

Start tutorial

Todo List

Create a complete todo-list application with document models, editors, and a custom Drive-app.

Start tutorial

Vetra Package Library

Explore the Vetra package library and learn how to leverage existing packages in your projects.

Explore library
--- ## Create a new chatroom project > Source: https://powerhouse.academy/academy/ExampleUsecases/Chatroom/CreateNewPowerhouseProject **TIP:** ๐Ÿ“ฆ **Reference Code**: [chatroom-demo](https://github.com/powerhouse-inc/chatroom-demo) This tutorial has a complete reference implementation available. You can: - View the complete code for each step - Clone and compare your implementation - Use `git diff` to compare your code with the reference :::
๐Ÿ“– How to use this tutorial This tutorial is designed for you to **build your own project from scratch** while having access to reference code. ### Setup: Create your project and connect to tutorial repo 1. **Create your project** following the tutorial: ```bash mkdir ph-projects cd ph-projects ph init # When prompted, enter project name: ChatRoom cd ChatRoom ``` 2. **Add the tutorial repository as a remote** to access reference code: ```bash git remote add tutorial https://github.com/powerhouse-inc/chatroom-demo.git git fetch tutorial --prune ``` 3. **Create your own branch** to keep your work organized: ```bash git checkout -b my-chatroom-project ``` Now you have access to the complete reference implementation while working on your own code! ### Compare your work with the reference At any point, compare what you've built with the reference: ```bash # Compare your current work with the reference git diff tutorial/main # Compare specific files git diff tutorial/main -- package.json ``` ### If you get stuck Reset your code to match the reference: ```bash # Reset to reference (WARNING: loses your changes) git reset --hard tutorial/main ```
## Overview This tutorial guides you through creating a **ChatRoom** application using Powerhouse. A Powerhouse project primarily consists of a document model and its editor. The ChatRoom demonstrates real-time collaboration features where users can post messages and react with emojis. ## Prerequisites - Powerhouse CLI installed: `pnpm install -g ph-cmd` or `npm install -g ph-cmd` - Node.js 24 and a package manager (pnpm or npm) installed - Visual Studio Code (or your preferred IDE) - Terminal/Command Prompt access If you need help with installing the prerequisites you can visit our page [prerequisites](/academy/MasteryTrack/BuilderEnvironment/Prerequisites) ## Before you begin 1. Open your terminal (either your system terminal or IDE's integrated terminal) 2. Optionally, create a folder first to keep your Powerhouse projects: ```bash mkdir ph-projects cd ph-projects ``` 3. Ensure you're in the correct directory before running the `ph init` command. In the terminal, you will be asked to enter the project name. Fill in the project name and press Enter. ```bash you@yourmachine:~/ph-projects % ph init ? What is the project name? โ€ฃ ChatRoom ``` Once the project is created, you will see the following output: ```bash Initialized empty Git repository in /Users/you/ph-projects/ChatRoom/.git/ The installation is done! ``` Navigate to the newly created project directory: ```bash cd ChatRoom ``` ## Develop your document model in Vetra Studio **Vetra Studio** is the builder's orchestration hub for assembling all specifications needed for your package. It provides a **Vetra Studio Drive** to access, manage, and share document model specifications, editors, and data integrationsโ€”all through a visual interface. For deeper coverage, see the [Vetra Studio documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio). Once in the project directory, run the `ph vetra --watch` command to start a Vetra Studio Drive where you'll be defining your specifications. ```bash ph vetra --watch ``` The host application for Vetra Studio will start and you will see the following output: ```bash โ„น [reactor-api] [package-manager] Loading packages: @powerhousedao/vetra โ„น [reactor-api] [server] WebSocket server available at /graphql/subscriptions โ„น [reactor-api] [graphql-manager] Registered /graphql/system subgraph. โ„น [reactor-api] [graphql-manager] Registered /graphql supergraph โ„น [reactor-api] [server] MCP server available at http://localhost:4001/mcp Switchboard initialized โžœ Drive URL: http://localhost:4001/d/vetra-bac239dd โžœ Local: http://localhost:3000/ โžœ Network: use --host to expose โžœ press h + enter to show help ``` A new browser window will open when visiting localhost and you will see the Vetra Studio Drive. If it doesn't open automatically, you can open it manually by navigating to `http://localhost:3000/` in your browser. Create a new document model by clicking the Document Models **'Add new specification'** button. Name your document `ChatRoom` (PascalCase, no spaces or hyphens). **Pay close attention to capitalization, as it influences code generation.** If you've followed the steps correctly, you'll have an empty `ChatRoom` document where you can define the **'Document Specifications'**.
Alternatively: Develop a single document model in Connect Once in the project directory, run the `ph connect` command to start a local instance of the Connect application. This allows you to start your document model specification document. ```bash ph connect ``` The Connect application will start and you will see the following output: ```bash โžœ Local: http://localhost:3000/ โžœ Network: http://192.168.5.110:3000/ โžœ press h + enter to show help ``` A new browser window will open and you will see the Connect application. If it doesn't open automatically, you can open it manually by navigating to `http://localhost:3000/` in your browser. **TIP:** If your local drive is not present, navigate to Settings in the bottom left corner. Settings > Danger Zone > Clear Storage. Clear the storage of your localhost application as it might have an old session cached. Move into your local drive. Create a new document model by clicking the `DocumentModel` button, found in the 'New Document' section at the bottom of the page. Name your document `ChatRoom` (PascalCase, no spaces or hyphens).
## Verify your setup At this point, your project structure should include: - Empty `document-models/`, `editors/`, `processors/`, and `subgraphs/` directories - Configuration files: `powerhouse.config.json`, `powerhouse.manifest.json` - Package management files: `package.json`, `pnpm-lock.yaml` - Build configuration: `tsconfig.json`, `vite.config.ts`, `vitest.config.ts` ## Up next In the next tutorial, you will learn how to design your document model and export it to be later used in your Powerhouse project. --- ## Write the document specification > Source: https://powerhouse.academy/academy/ExampleUsecases/Chatroom/DefineChatroomDocumentModel **TIP:** ๐Ÿ“ฆ **Reference Code**: [chatroom-demo](https://github.com/powerhouse-inc/chatroom-demo) This tutorial step has a corresponding implementation in the repository. After completing this step, your project will have a document model specification with: - Document model specification files (`chat-room.json`, `schema.graphql`) - Auto-generated TypeScript types and action creators - Reducer scaffolding ready for implementation :::
๐Ÿ“– How to use this tutorial **Prerequisites**: Complete step 1 and set up the tutorial remote (see previous step). ### Compare your generated code As Vetra generates code automatically, compare with the reference: ```bash # Compare all generated files with the reference git diff tutorial/main -- document-models/chat-room/ # View a specific file from the reference git show tutorial/main:document-models/chat-room/schema.graphql ``` ### Visual comparison with GitHub Desktop After making a commit, use GitHub Desktop for visual diff: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/main` 3. Review all file differences in the visual interface See step 1 for detailed GitHub Desktop instructions.
In this tutorial, you will learn how to define the specifications for a **ChatRoom** document model within Vetra Studio using its GraphQL schema, and then export the resulting document model specification document for your Powerhouse project. If you don't have a document specification file created yet, have a look at the previous step of this tutorial to create a new document specification. Before you start, make sure you have Vetra Studio running locally with the command: ```bash ph vetra --watch ``` ## ChatRoom document specification Make sure you have named your document model `ChatRoom` (PascalCase, no spaces or hyphens). **Pay close attention to capitalization, as it influences code generation.** We use the **GraphQL Schema Definition Language** (SDL) to define the schema for the document model. Below, you can see the SDL for the `ChatRoom` document model. **INFO:** This schema defines the **data structure** of the document model and the types involved in its operations. Documents in Powerhouse leverage **event sourcing principles**, where every state transition is represented by an operation. GraphQL input types describe operations, ensuring that user intents are captured effectively.
State schema of our ChatRoom ```graphql type ChatRoomState { id: OID! name: String! description: String createdAt: DateTime createdBy: ID messages: [Message!]! } type Message { id: OID! sender: Sender! content: String sentAt: DateTime! reactions: [Reaction!] } type Sender { id: ID! name: String avatarUrl: URL } type Reaction { type: ReactionType! reactedBy: [ID!]! } enum ReactionType { THUMBS_UP THUMBS_DOWN LAUGH HEART CRY } ```
Messages Module: Operations for ChatRoom Messages ```graphql # Add a new message to the chat-room input AddMessageInput { messageId: OID! sender: SenderInput! content: String! sentAt: DateTime! } # Sender information for a message input SenderInput { id: ID! name: String avatarUrl: URL } # Add an emoji reaction to a message input AddEmojiReactionInput { messageId: OID! reactedBy: ID! type: ReactionType! } # Remove an emoji reaction from a message input RemoveEmojiReactionInput { messageId: OID! senderId: ID! type: ReactionType! } ```
Settings Module: Operations for ChatRoom Settings ```graphql # Edit the chat-room name input EditChatNameInput { name: String } # Edit the chat-room description input EditChatDescriptionInput { description: String } ```
## Define the document model specification To define the document model, you need to open the document model editor in Vetra Studio. ### Steps to define your document model: 1. In Vetra Studio, click on **'ChatRoom' Document** to open the document model specification editor. 2. You'll be presented with a form to fill in metadata about the document model. Fill in the details in the respective fields. In the **Document Type** field, type `powerhouse/chat-room` (lowercase with hyphen). This defines the new type of document that will be created with this document model specification. ![Chatroom Document Model Form Metadata](image-2.png) 3. In the code editor, you can see the SDL for the document model. Replace the existing SDL template with the SDL defined in the **State Schema** section above. Only copy and paste the types, leaving the inputs for the next step. You can press the 'Sync with schema' button to set the initial state of your document model based on your Schema Definition Language. 4. Verify that your **Global State Initial Value** looks like this: ```json { "ID": "", "name": "", "description": null, "createdAt": null, "createdBy": null, "messages": [] } ``` 5. Below the editor, find the input field `Add module`. Create the first module for message-related operations. Name the module `messages`. Press enter. 6. Now there is a new field, called `Add operation`. Here you will add each input operation to the module, one by one. 7. Inside the `Add operation` field, type `ADD_MESSAGE` and press enter. A small editor will appear underneath with an empty input type that you need to fill. Copy the `AddMessageInput` and `SenderInput` from the **Messages Module** section and paste them in the editor: ```graphql input AddMessageInput { messageId: OID! sender: SenderInput! content: String! sentAt: DateTime! } input SenderInput { id: ID! name: String avatarUrl: URL } ``` 8. Add the remaining message operations to the `messages` module: `ADD_EMOJI_REACTION` and `REMOVE_EMOJI_REACTION`. Note that you only need to add the operation name (e.g., `ADD_EMOJI_REACTION`) without the `Input` suffixโ€”it will be generated automatically. 9. Add **reducer exceptions** to the `ADD_MESSAGE` operation for validation: `MessageContentCannotBeEmptyError` and `MessageNotFoundError`. These will be used later to validate messages. 10. Create a second module called `settings` for the chat room configuration operations. 11. Add the settings operations to the `settings` module: `EDIT_CHAT_NAME` and `EDIT_CHAT_DESCRIPTION`. 12. In the meantime, Vetra has been keeping an eye on your inputs and started code generation in your directory. In your terminal you will also find any validation errors that help you to identify missing specifications. ## Verify your document model generation If you have been watching the terminal in your IDE you will see that Vetra has been tracking your changes and scaffolding your directory. Your project should have the following structure in `document-models/chat-room/`: ``` document-models/chat-room/ โ”œโ”€โ”€ gen/ # Auto-generated code (don't edit) โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”œโ”€โ”€ creators.ts # Action creator functions โ”‚ โ”œโ”€โ”€ types.ts # TypeScript type definitions โ”‚ โ”œโ”€โ”€ reducer.ts โ”‚ โ”œโ”€โ”€ messages/ # Messages module โ”‚ โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”‚ โ”œโ”€โ”€ error.ts # Error classes for validation โ”‚ โ”‚ โ””โ”€โ”€ operations.ts โ”‚ โ””โ”€โ”€ settings/ # Settings module โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”œโ”€โ”€ error.ts โ”‚ โ””โ”€โ”€ operations.ts โ”œโ”€โ”€ src/ # Your custom implementation โ”‚ โ”œโ”€โ”€ reducers/ โ”‚ โ”‚ โ”œโ”€โ”€ messages.ts # Message operation reducers โ”‚ โ”‚ โ””โ”€โ”€ settings.ts # Settings operation reducers โ”‚ โ””โ”€โ”€ tests/ โ”‚ โ”œโ”€โ”€ document-model.test.ts # Document model tests โ”‚ โ”œโ”€โ”€ messages.test.ts # Messages operation tests โ”‚ โ””โ”€โ”€ settings.test.ts # Settings operation tests โ”œโ”€โ”€ chat-room.json # Document model specification โ””โ”€โ”€ schema.graphql # GraphQL schema ``` ### Compare with reference Verify your generated files match the expected structure: ```bash # Compare your generated files with the reference git diff tutorial/main -- document-models/chat-room/ # List what was generated in the reference git ls-tree -r --name-only tutorial/main document-models/chat-room/ ``` ## Up next: Reducers In the next step, you'll learn how to implement the runtime logic that will use the `ChatRoom` document model specification you've just created. --- ## Implement the document model reducers > Source: https://powerhouse.academy/academy/ExampleUsecases/Chatroom/ImplementOperationReducers **TIP:** ๐Ÿ“ฆ **Reference Code**: [chatroom-demo](https://github.com/powerhouse-inc/chatroom-demo) This tutorial covers two key implementations: 1. **Reducers**: Implementing the reducer logic for all ChatRoom operations 2. **Tests**: Writing comprehensive tests for the reducers You can view the exact implementation in the repository's `document-models/chat-room/src/` directory.
๐Ÿ“– How to use this tutorial This tutorial covers implementing reducers and tests. ### Compare your reducer implementation After implementing your reducers: ```bash # Compare your reducers with the reference git diff tutorial/main -- document-models/chat-room/src/reducers/ # View the reference reducer implementation git show tutorial/main:document-models/chat-room/src/reducers/messages.ts ``` ### Compare your tests After writing tests: ```bash # Compare your tests with the reference git diff tutorial/main -- document-models/chat-room/src/tests/ # View the reference test implementation git show tutorial/main:document-models/chat-room/src/tests/messages.test.ts ``` ### Visual comparison with GitHub Desktop After committing your work, compare visually: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/main` 3. Review differences in the visual interface See step 1 for detailed GitHub Desktop instructions. ### If you get stuck View or reset to the reference: ```bash # View the reducer code from the reference git show tutorial/main:document-models/chat-room/src/reducers/messages.ts # Reset to reference (WARNING: loses your changes) git reset --hard tutorial/main ```
In this section, we will implement and test the operation reducers for the **ChatRoom** document model. Vetra Studio has been automatically generating your document model code as you defined the specification. If you need to review the document model specification steps, see [Define ChatRoom Document Model](/academy/ExampleUsecases/Chatroom/DefineChatroomDocumentModel). ## Understanding reducers in document models Reducers are a core concept in Powerhouse document models. They implement the state transition logic for each operation defined in your schema. **INFO:** **Connection to schema definition language (SDL)**: The reducers directly implement the operations you defined in your SDL. Remember how we defined `AddMessageInput`, `AddEmojiReactionInput`, `RemoveEmojiReactionInput`, `EditChatNameInput`, and `EditChatDescriptionInput` in our schema? The reducers provide the actual implementation of what happens when those operations are performed. To import the document model specification into your Powerhouse project, you can either: - Copy and paste the file directly into the root of your Powerhouse project. - Or drag and drop the file into the Powerhouse project directory in the VSCode editor as seen in the image below: Either step will import the document model specification into your Powerhouse project. ![vscode image](image-4.png) ## In your project directory The next steps will take place in the VSCode editor. Make sure to have it open and the terminal window inside VSCode open as well. To write the operation reducers of the **ChatRoom** document model, you need to generate the document model code from the document model specification file you have exported into the Powerhouse project directory. To do this, run the following command in the terminal: ```bash ph generate ChatRoom.phd ``` You will see that this action created a range of files for you. Before diving in, let's look at this simple schema to familiarize yourself with the structure you've defined in the document model once more. It shows how each type is connected to the next one. ![Chatroom-demo Schema](image.png) ## Implement the messages reducers Navigate to `/document-models/chat-room/src/reducers/messages.ts` and start writing the operation reducers for the messages module. Open the `messages.ts` file and you should see the scaffolding code that needs to be filled for the three message operations. The generated file will look like this: ```typescript export const chatRoomMessagesOperations: ChatRoomMessagesOperations = { addMessageOperation(state, action) { // TODO: Implement "addMessageOperation" reducer throw new Error('Reducer "addMessageOperation" not yet implemented'); }, addEmojiReactionOperation(state, action) { // TODO: Implement "addEmojiReactionOperation" reducer throw new Error('Reducer "addEmojiReactionOperation" not yet implemented'); }, removeEmojiReactionOperation(state, action) { // TODO: Implement "removeEmojiReactionOperation" reducer throw new Error( 'Reducer "removeEmojiReactionOperation" not yet implemented', ); }, }; ``` ### Write the messages operation reducers Copy and paste the code below into the `messages.ts` file in the `reducers` folder, replacing the scaffolding code:
Messages Operation Reducers ```typescript MessageNotFoundError, MessageContentCannotBeEmpty, } from "../../gen/messages/error.js"; export const chatRoomMessagesOperations: ChatRoomMessagesOperations = { addMessageOperation(state, action) { if (action.input.content === "") { throw new MessageContentCannotBeEmpty(); } const newMessage = { id: action.input.messageId, sender: { id: action.input.sender.id, name: action.input.sender.name || null, avatarUrl: action.input.sender.avatarUrl || null, }, content: action.input.content, sentAt: action.input.sentAt, reactions: [], }; state.messages.push(newMessage); }, addEmojiReactionOperation(state, action) { const message = state.messages.find((m) => m.id === action.input.messageId); if (!message) { throw new MessageNotFoundError( `Message with ID ${action.input.messageId} not found`, ); } if (!message.reactions) { message.reactions = []; } const existingReaction = message.reactions.find( (r) => r.type === action.input.type, ); if (existingReaction) { if (!existingReaction.reactedBy.includes(action.input.reactedBy)) { existingReaction.reactedBy.push(action.input.reactedBy); } } else { message.reactions.push({ type: action.input.type, reactedBy: [action.input.reactedBy], }); } }, removeEmojiReactionOperation(state, action) { const message = state.messages.find((m) => m.id === action.input.messageId); if (!message) { throw new MessageNotFoundError( `Message with ID ${action.input.messageId} not found`, ); } if (!message.reactions) { return; } const reactionIndex = message.reactions.findIndex( (r) => r.type === action.input.type, ); if (reactionIndex === -1) { return; } const reaction = message.reactions[reactionIndex]; const userIndex = reaction.reactedBy.indexOf(action.input.senderId); if (userIndex !== -1) { reaction.reactedBy.splice(userIndex, 1); if (reaction.reactedBy.length === 0) { message.reactions.splice(reactionIndex, 1); } } }, senderOperation(state, action) { // TODO: Implement "senderOperation" reducer throw new Error('Reducer "senderOperation" not yet implemented'); }, }; ```
## Write the operation reducer tests In order to make sure the operation reducers are working as expected, you need to write tests for them. Navigate to `/document-models/chat-room/src/tests` and you'll find test files for each module. Replace the scaffolding code with the tests below. ### Messages operation tests Replace the content of `messages.test.ts` with:
Messages Operation Tests ```typescript /** * This is a scaffold file meant for customization: * - change it by adding new tests or modifying the existing ones */ reducer, utils, isChatRoomDocument, addMessage, AddMessageInputSchema, addEmojiReaction, AddEmojiReactionInputSchema, removeEmojiReaction, RemoveEmojiReactionInputSchema, } from "chatroom/document-models/chat-room"; describe("Messages Operations", () => { it("should handle addMessage operation", () => { const document = utils.createDocument(); const input = generateMock(AddMessageInputSchema()); const updatedDocument = reducer(document, addMessage(input)); expect(isChatRoomDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_MESSAGE", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle addEmojiReaction operation", () => { const document = utils.createDocument(); const input = generateMock(AddEmojiReactionInputSchema()); const updatedDocument = reducer(document, addEmojiReaction(input)); expect(isChatRoomDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_EMOJI_REACTION", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle removeEmojiReaction operation", () => { const document = utils.createDocument(); const input = generateMock(RemoveEmojiReactionInputSchema()); const updatedDocument = reducer(document, removeEmojiReaction(input)); expect(isChatRoomDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "REMOVE_EMOJI_REACTION", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); }); ```
### Document model tests The `document-model.test.ts` file contains tests to verify the document model structure. Replace its content with:
Document Model Tests ```typescript /** * This is a scaffold file meant for customization: * - change it by adding new tests or modifying the existing ones */ utils, initialGlobalState, initialLocalState, chatRoomDocumentType, isChatRoomDocument, assertIsChatRoomDocument, isChatRoomState, assertIsChatRoomState, } from "chatroom/document-models/chat-room"; describe("ChatRoom Document Model", () => { it("should create a new ChatRoom document", () => { const document = utils.createDocument(); expect(document).toBeDefined(); expect(document.header.documentType).toBe(chatRoomDocumentType); }); it("should create a new ChatRoom document with a valid initial state", () => { const document = utils.createDocument(); expect(document.state.global).toStrictEqual(initialGlobalState); expect(document.state.local).toStrictEqual(initialLocalState); expect(isChatRoomDocument(document)).toBe(true); expect(isChatRoomState(document.state)).toBe(true); }); it("should reject a document that is not a ChatRoom document", () => { const wrongDocumentType = utils.createDocument(); wrongDocumentType.header.documentType = "the-wrong-thing-1234"; try { expect(assertIsChatRoomDocument(wrongDocumentType)).toThrow(); expect(isChatRoomDocument(wrongDocumentType)).toBe(false); } catch (error) { expect(error).toBeInstanceOf(ZodError); } }); const wrongState = utils.createDocument(); // @ts-expect-error - we are testing the error case wrongState.state.global = { ...{ notWhat: "you want" }, }; try { expect(isChatRoomState(wrongState.state)).toBe(false); expect(assertIsChatRoomState(wrongState.state)).toThrow(); expect(isChatRoomDocument(wrongState)).toBe(false); expect(assertIsChatRoomDocument(wrongState)).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } const wrongInitialState = utils.createDocument(); // @ts-expect-error - we are testing the error case wrongInitialState.initialState.global = { ...{ notWhat: "you want" }, }; try { expect(isChatRoomState(wrongInitialState.state)).toBe(false); expect(assertIsChatRoomState(wrongInitialState.state)).toThrow(); expect(isChatRoomDocument(wrongInitialState)).toBe(false); expect(assertIsChatRoomDocument(wrongInitialState)).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } const missingIdInHeader = utils.createDocument(); // @ts-expect-error - we are testing the error case delete missingIdInHeader.header.id; try { expect(isChatRoomDocument(missingIdInHeader)).toBe(false); expect(assertIsChatRoomDocument(missingIdInHeader)).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } const missingNameInHeader = utils.createDocument(); // @ts-expect-error - we are testing the error case delete missingNameInHeader.header.name; try { expect(isChatRoomDocument(missingNameInHeader)).toBe(false); expect(assertIsChatRoomDocument(missingNameInHeader)).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } const missingCreatedAtUtcIsoInHeader = utils.createDocument(); // @ts-expect-error - we are testing the error case delete missingCreatedAtUtcIsoInHeader.header.createdAtUtcIso; try { expect(isChatRoomDocument(missingCreatedAtUtcIsoInHeader)).toBe(false); expect(assertIsChatRoomDocument(missingCreatedAtUtcIsoInHeader)).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } const missingLastModifiedAtUtcIsoInHeader = utils.createDocument(); // @ts-expect-error - we are testing the error case delete missingLastModifiedAtUtcIsoInHeader.header.lastModifiedAtUtcIso; try { expect(isChatRoomDocument(missingLastModifiedAtUtcIsoInHeader)).toBe(false); expect( assertIsChatRoomDocument(missingLastModifiedAtUtcIsoInHeader), ).toThrow(); } catch (error) { expect(error).toBeInstanceOf(ZodError); } }); ```
## Run the tests Now you can run the tests to make sure the operation reducers are working as expected. ```bash pnpm run test ``` Output should be similar to: ```bash โœ“ document-models/chat-room/src/tests/document-model.test.ts (3 tests) 1ms โœ“ document-models/chat-room/src/tests/messages.test.ts (3 tests) 8ms โœ“ document-models/chat-room/src/tests/settings.test.ts (2 tests) 2ms Test Files 3 passed (3) Tests 8 passed (8) Start at 15:19:52 Duration 3.61s (transform 77ms, setup 0ms, collect 3.50s, tests 14ms, environment 0ms, prepare 474ms) ``` If you got a similar output, you have successfully implemented the operation reducers and tests for the **ChatRoom** document model. ## Compare with reference implementation Verify your implementation matches the tutorial: ```bash # View reference reducer implementation git show tutorial/main:document-models/chat-room/src/reducers/messages.ts git show tutorial/main:document-models/chat-room/src/reducers/settings.ts # View reference test implementation git show tutorial/main:document-models/chat-room/src/tests/messages.test.ts git show tutorial/main:document-models/chat-room/src/tests/settings.test.ts # Compare your entire implementation git diff tutorial/main -- document-models/chat-room/src/ ``` ## Up next: ChatRoom editor Continue to the next section to learn how to implement the document model editor so you can see a simple user interface for the **ChatRoom** document model in action. --- ## Build the ChatRoom editor > Source: https://powerhouse.academy/academy/ExampleUsecases/Chatroom/ImplementChatroomEditor **TIP:** ๐Ÿ“ฆ **Reference Code**: [chatroom-demo](https://github.com/powerhouse-inc/chatroom-demo) This tutorial covers building the ChatRoom editor: 1. **Editor Scaffolding**: Generating the editor template with `ph generate --editor` 2. **Component Implementation**: Building a complete, interactive chat UI with components Explore the complete implementation in the `editors/chat-room-editor/` directory.
๐Ÿ“– How to use this tutorial This tutorial shows building from **generated scaffolding** to a **complete chat UI**. ### Compare your generated editor ```bash # Compare generated scaffolding with the reference git diff tutorial/main -- editors/chat-room-editor/ # View the generated editor template git show tutorial/main:editors/chat-room-editor/editor.tsx ``` ### Browse the complete implementation Explore the production-ready component structure: ```bash # List all components in the reference git ls-tree -r --name-only tutorial/main editors/chat-room-editor/components/ # View a specific component git show tutorial/main:editors/chat-room-editor/components/ChatRoom/ChatRoom.tsx ``` ### Visual comparison with GitHub Desktop After committing your editor code: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/main` 3. See all your custom components vs. the reference implementation See step 1 for detailed GitHub Desktop instructions.
In this chapter we will continue with the interface or editor implementation of the **ChatRoom** document model. This means you will create a user interface for the **ChatRoom** document model which will be used to visualize your chatroom, send messages, and react with emojis. ## Add a document editor specification in Vetra Studio Go back to Vetra Studio, if you need to relaunch Vetra, launch it with `Vetra --watch` so it loads all existing local documents. Click the **'Add new specification'** button in the User Experiences column under **'Editors'**. This will create an editor template for your document model. Give the editor the name `chat-room-editor` and select the correct document model. In our case that's the `powerhouse/chat-room`. You'll see that the terminal in which you are running Vetra mentions ``` โ„น [Vetra] ๐Ÿ”„ Starting editor generation for: chat-room-editor added: editors/chat-room-editor/components/EditName.tsx added: editors/chat-room-editor/editor.tsx FORCED: editors/chat-room-editor/module.ts โ„น [Vetra] โœ… Editor generation completed successfully for: chat-room-editor ``` Once complete, you'll have a new directory structure: ``` editors/chat-room-editor/ โ”œโ”€โ”€ components/ โ”‚ โ””โ”€โ”€ EditName.tsx # Auto-generated component for editing document name โ”œโ”€โ”€ editor.tsx # Main editor component (to be customized) โ””โ”€โ”€ module.ts # Editor module configuration ``` Navigate to the `editors/chat-room-editor/editor.tsx` file and open it in your editor. You'll see a basic template ready for customization. ### Editor implementation options When building your editor component within the Powerhouse ecosystem, you have several options for styling: 1. **Default HTML Styling:** Standard HTML tags will render with default styles offered through the boilerplate. 2. **Tailwind CSS:** Vetra Studio comes with Tailwind CSS integrated. You can directly use Tailwind utility classes. 3. **Custom CSS Files:** You can import traditional CSS files to apply custom styles. Vetra Studio Preview provides a dynamic local environment. By running `ph vetra --watch`, you can visualize your components instantly as you build them. ## Build the editor with components We'll build the editor using a component-based approach for better organization and reusability. ### Component-based architecture The ChatRoom editor uses a modular component structure. Each component has its own folder with an `index.ts` file for clean exports: ``` editors/chat-room-editor/ โ”œโ”€โ”€ components/ โ”‚ โ”œโ”€โ”€ Avatar/ # User avatar display โ”‚ โ”‚ โ”œโ”€โ”€ Avatar.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ ChatRoom/ # Main chat container โ”‚ โ”‚ โ”œโ”€โ”€ ChatRoom.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ Header/ # Chat header with editable title/description โ”‚ โ”‚ โ”œโ”€โ”€ EditableLabel.tsx โ”‚ โ”‚ โ”œโ”€โ”€ Header.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ Message/ # Individual message bubble โ”‚ โ”‚ โ”œโ”€โ”€ Message.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ MessageItem/ # Message with avatar and reaction dropdown โ”‚ โ”‚ โ”œโ”€โ”€ MessageItem.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ Reaction/ # Emoji reaction display โ”‚ โ”‚ โ”œโ”€โ”€ Reaction.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ TextInput/ # Message input field โ”‚ โ”‚ โ”œโ”€โ”€ SendIcon.tsx โ”‚ โ”‚ โ”œโ”€โ”€ TextInput.tsx โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ””โ”€โ”€ index.ts # Central exports โ”œโ”€โ”€ editor.tsx # Main editor component โ”œโ”€โ”€ utils.ts # Utility functions for data mapping โ””โ”€โ”€ module.ts # Editor module configuration ``` ### Copy components from the reference repository Download the repository of the chatroom-demo as a zip file from https://github.com/powerhouse-inc/chatroom-demo and navigate to `editors/chat-room-editor/` to copy the following: 1. **The entire `components/` folder** - Contains all UI components 2. **The `utils.ts` file** - Contains utility functions for emoji mapping Here's what each component does: | Component | Purpose | | --------------- | --------------------------------------------------------------------------- | | `Avatar` | Displays a user avatar image or a deterministic emoji based on the username | | `ChatRoom` | Main container that orchestrates the header, messages list, and input field | | `Header` | Shows the chat title and description with inline editing capability | | `EditableLabel` | Reusable component for inline text editing with edit/cancel icons | | `Message` | Renders a single message bubble with styling based on the sender | | `MessageItem` | Wraps `Message` with `Avatar` and adds a reaction dropdown menu | | `Reaction` | Displays an emoji reaction with a count of users who reacted | | `TextInput` | Input field for composing and sending new messages | ### The utils.ts file The `utils.ts` file contains helper functions for mapping between document model types and component props:
View utils.ts code ```typescript MessageProps, ReactionMap, } from "./components/Message/Message.js"; Message, ReactionType, } from "../../document-models/chat-room/gen/schema/types.js"; const emojis = [ "๐Ÿ˜€", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜", "๐Ÿ˜Ž", "๐Ÿ˜Š", "๐Ÿ™ƒ", "๐Ÿ˜‡", "๐Ÿค”", "๐Ÿฅณ", "๐Ÿคฏ", "๐Ÿค—", "๐Ÿ˜ฑ", "๐Ÿ‘ป", "๐ŸŽƒ", "๐Ÿฑ", "๐Ÿถ", "๐Ÿน", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฏ", "๐Ÿฆ", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿ", "๐Ÿž", "๐ŸŸ", "๐Ÿฌ", "๐Ÿณ", "๐Ÿฆ‹", "๐ŸŒบ", "๐ŸŒธ", "๐ŸŒผ", "๐Ÿ€", ]; export function getEmojiFromString(input: string): string { function hashString(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; } return Math.abs(hash); } const hash = hashString(input); return emojis[hash % emojis.length]; } export const reactionTypeToEmoji = (reactionType: ReactionType): string => { switch (reactionType) { case "HEART": return "โค๏ธ"; case "THUMBS_UP": return "๐Ÿ‘"; case "THUMBS_DOWN": return "๐Ÿ‘Ž"; case "LAUGH": return "๐Ÿ˜‚"; case "CRY": return "๐Ÿ˜ข"; default: return "โค๏ธ"; } }; export const reactionTypeToReactionKey = ( reactionType: ReactionType, ): keyof ReactionMap => { switch (reactionType) { case "HEART": return "heart"; case "THUMBS_UP": return "thumbsUp"; case "THUMBS_DOWN": return "thumbsDown"; case "LAUGH": return "laughing"; case "CRY": return "cry"; default: return "heart"; } }; export const reactionKeyToReactionType = ( reactionKey: string, ): ReactionType => { switch (reactionKey) { case "heart": return "HEART"; case "thumbsUp": return "THUMBS_UP"; case "thumbsDown": return "THUMBS_DOWN"; case "laughing": return "LAUGH"; case "cry": return "CRY"; default: return "HEART"; } }; export const mapReactions = ( reactions: Message["reactions"], ): MessageProps["reactions"] => { return (reactions || []) .map((reaction) => ({ emoji: reactionTypeToEmoji(reaction.type), reactedBy: reaction.reactedBy, type: reactionTypeToReactionKey(reaction.type), })) .filter((reaction) => reaction.reactedBy.length > 0); }; ```
### The main editor.tsx file The main `editor.tsx` file connects your document model to the UI components. Replace the generated scaffolding with the code underneath:
View editor.tsx code ```typescript addMessage, addEmojiReaction, removeEmojiReaction, editChatName, editChatDescription, } from "../../document-models/chat-room/gen/creators.js"; ChatRoom, type ChatRoomProps, type MessageProps, } from "./components/index.js"; export default function Editor() { const [document, dispatch] = useSelectedChatRoomDocument(); const user = useUser(); const disableChatRoom = !user; if (!document) { return
Loading...
; } const messages: ChatRoomProps["messages"] = document.state.global.messages.map((message) => ({ id: message.id, message: message.content || "", timestamp: message.sentAt, userName: message.sender.name || message.sender.id, imgUrl: message.sender.avatarUrl || undefined, isCurrentUser: message.sender.id === user?.address, reactions: mapReactions(message.reactions), })); const onSendMessage: ChatRoomProps["onSendMessage"] = (message) => { if (!message) { return; } dispatch( addMessage({ messageId: generateId(), content: message, sender: { id: user?.address || "anon-user", name: user?.ens?.name || null, avatarUrl: user?.ens?.avatarUrl || null, }, sentAt: new Date().toISOString(), }), ); }; const addReaction = ( messageId: string, userId: string, reactionType: "HEART" | "THUMBS_UP" | "THUMBS_DOWN" | "LAUGH" | "CRY", ) => { dispatch( addEmojiReaction({ messageId, reactedBy: userId, type: reactionType, }), ); }; const removeReaction = ( messageId: string, userId: string, reactionType: "HEART" | "THUMBS_UP" | "THUMBS_DOWN" | "LAUGH" | "CRY", ) => { dispatch( removeEmojiReaction({ messageId, senderId: userId, type: reactionType, }), ); }; const onClickReaction: MessageProps["onClickReaction"] = (reaction) => { const message = messages.find( (message) => message.id === reaction.messageId, ); if (!message) { return; } const messageId = reaction.messageId; const reactionType = reactionKeyToReactionType(reaction.type); const currentUserId = user?.address || "anon-user"; const existingReaction = message.reactions?.find( (r) => r.type === reaction.type, ); if (existingReaction) { const dispatchAction = existingReaction.reactedBy.includes(currentUserId) ? removeReaction : addReaction; dispatchAction(messageId, currentUserId, reactionType); } else { addReaction(messageId, currentUserId, reactionType); } }; const onSubmitTitle: ChatRoomProps["onSubmitTitle"] = (title) => { dispatch(editChatName({ name: title })); }; const onSubmitDescription: ChatRoomProps["onSubmitDescription"] = ( description, ) => { dispatch(editChatDescription({ description })); }; return (
); } ```
**What's happening here:** - We use `useSelectedChatRoomDocument` hook to get the document state and dispatch function - We use `useUser` to get the current user information (for authentication) - We map the document's messages to props that our ChatRoom component expects - We create handlers for sending messages, adding/removing reactions, and editing metadata - We dispatch operations (`addMessage`, `addEmojiReaction`, etc.) from our generated creators **INFO:** The `useSelectedChatRoomDocument` hook is generated by the Powerhouse CLI. It provides: 1. The current document state (`document`) 2. A dispatch function to send actions to the reducer This hook connects your React components to the document model's state and operations. ## Key components explained ### MessageItem component The `MessageItem` component wraps the `Message` component with an avatar and a reaction dropdown menu. It uses the `@powerhousedao/design-system` package for the dropdown: ```typescript DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem, } from "@powerhousedao/design-system"; export type MessageItemProps = MessageProps & AvatarProps; export const reactionMap = { cry: "๐Ÿ˜ข", laughing: "๐Ÿ˜‚", heart: "โค๏ธ", thumbsDown: "๐Ÿ‘Ž", thumbsUp: "๐Ÿ‘", }; export const MessageItem: React.FC = (props) => { const { imgUrl, userName, isCurrentUser, ...messageProps } = props; const { disabled = false } = messageProps; const [isHovered, setIsHovered] = useState(false); const [open, setOpen] = useState(false); // ... hover and dropdown logic return (
๐Ÿซฅ {Object.entries(reactionMap).map(([key, emoji]) => ( /* handle reaction */}> {emoji} ))}
); }; ``` ### EditableLabel component The `EditableLabel` component enables inline editing of text fields (like the chat title and description): ```typescript export const EditableLabel: React.FC = ({ label: initialLabel, onSubmit, style, }) => { const [hover, setHover] = useState(false); const [isEditing, setIsEditing] = useState(false); const [label, setLabel] = useState(initialLabel); // Toggle between read mode (displaying text) and write mode (input field) // Press Enter to submit, Escape to cancel return (
setHover(true)} onMouseLeave={() => setHover(false)}> {isEditing ? ( setLabel(e.target.value)} /> ) : (

{label}

)} {(hover || isEditing) && setIsEditing(true)} />}
); }; ``` ## Test your editor Now you can run Vetra Studio Preview and see the **ChatRoom** editor in action: ```bash ph vetra --watch ``` In Vetra Studio, in the bottom right corner you'll find a new Document Model that you can create: **ChatRoom**. Click on it to create a new ChatRoom document. **WARNING:** A warning will prompt you to login before you can send messages. Login with an Ethereum address via Renown to start sending messages. ![Chatroom Editor](../docs/docs/images/ChatRoomTest.gif) **Try it out:** 1. Create a new ChatRoom document 2. Login with your Ethereum wallet 3. Send messages using the input field 4. React to messages with emoji reactions 5. Click the chat name or description to edit them Congratulations! ๐ŸŽ‰ If you managed to follow this tutorial until this point, you have successfully implemented the **ChatRoom** document model with its reducer operations and editor. ## Compare with the reference implementation The tutorial repository includes the complete ChatRoom editor with all components: ```bash # See the ChatRoom component implementation git show tutorial/main:editors/chat-room-editor/components/ChatRoom/ChatRoom.tsx # Explore the MessageItem component git show tutorial/main:editors/chat-room-editor/components/MessageItem/MessageItem.tsx # View the EditableLabel component git show tutorial/main:editors/chat-room-editor/components/Header/EditableLabel.tsx # Compare your implementation with the reference git diff tutorial/main -- editors/chat-room-editor/ ``` ## Key concepts learned In this tutorial you've learned: โœ… **Component-based architecture** - Breaking down complex UIs into reusable components โœ… **Document model hooks** - Using `useSelectedChatRoomDocument` to connect React to your document state โœ… **User authentication** - Using `useUser` hook for wallet-based authentication โœ… **Action dispatching** - How to dispatch operations from your UI โœ… **Type-safe development** - Leveraging TypeScript with generated types from your SDL โœ… **Real-time collaboration** - Building features that work across multiple users --- ## Step 0 - get the starter code > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/GetTheStarterCode Normally you would initialize a new powerhouse project by running `ph init` with your project name. But since this is a tutorial, we want to provide branches with the final code for each step. Just for the tutorial, please instead make a fork of [this repository](https://github.com/powerhouse-inc/todo-tutorial). _NOTE:_ please _uncheck_ the checkbox that says "copy the main branch only" when making your fork โ€” we want to keep the other branches for each step. Once you have your fork, clone it to your machine with `git clone`, the GitHub desktop app or the GitHub cli. The starter branch of this tutorial is: `step-1-generate-todo-list-document-model`. Checkout that branch, and then create your own branch from it with whatever name you want, something like `do-the-tutorial` will work nicely. The code at the step 1 branch of this repository is exactly the same as what you would get if you ran `ph init todo-tutorial`. Each step in this tutorial has two branches associated with it. One is the starting point and the other is the final code after the step is complete. They each have names like `step-1-` for the starting point and `step-1-complete-` for the complete code. You can use the starting point branches if you want to start at a later step or skip a step, and you can use the complete code to compare with your branch if you get stuck. To compare your branch, either do `git diff my-branch step-complete-branch` or use the "compare with branch" option in the GitHub desktop app. Finally, run `pnpm install` to install the project dependencies. Now we're ready to get started. --- ## Step 1 - Generate the `TodoList` document model > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/GenerateTodoListDocumentModel ## Overview This tutorial guides you through creating a simplified version of a 'Powerhouse project' for a **To-do List**. A Powerhouse project primarily consists of a document model and its editor. As your projects use-case expands you can add data-integrations or a specific drive-app as seen in the demo package. For todays purpose, you'll be using Connect, our user-centric collaboration tool and Vetra Studio, the builder tooling through which developers can access and manage specifications of your project. ## Develop a single document model in Connect Once in the project directory, run the `pnpm connect` command to start a local instance of the Connect application. This allows you to start your document model specification document. Run the following command to start the Connect application: ```bash pnpm connect ``` The Connect application will start and you will see the following output: ```bash โžœ Local: http://localhost:3000/ โžœ Network: http://192.168.5.110:3000/ โžœ press h + enter to show help ``` A new browser window will open and you will see the Connect application. If it doesn't open automatically, you can open it manually by navigating to `http://localhost:3000/` in your browser. You will see your local drive and a button to create a new drive. **TIP:** If you local drive is not present navigate into Settings in the bottom left corner. Settings > Danger Zone > Clear Storage. Clear the storage of your localhost application as it might has an old session cached. 1. To create a new local drive, click the "Create New Drive" icon. In the "Drive Name" field, enter a name for your drive (we will use "local" as the name). For "Drive App", select "Generic drive explorer" and for "Location", choose "Local". Then click the "Create new drive" button at the bottom of the modal. 2. Move into your local drive, by clicking the the name of the drive (local). Create a new document model by clicking the `DocumentModel` button, found in the 'New Document' section at the bottom of the page. Name your document `Todo List`. If you've followed the steps correctly, you'll have an empty `Todo List` document where you can define the **'Document Specifications'**. ## TodoList document specification To start, fill in the following details for your new document model: Name: `Todo List` Document type: `powerhouse/todo-list` Author name: Powerhouse Website URL: https://powerhouse.inc It's important that you use these exact details so that your generated code matches the generated code in the tutorial repository. We'll continue with this project to teach you how to create a document model specification and later an editor for your document model. We use the **GraphQL Schema Definition Language** (SDL) to define the schema for the document model. Below, you can see the SDL for the `TodoList` document model. **INFO:** This schema defines the **data structure** of the document model and the types involved in its operations, which are detailed further as input types. Documents in Powerhouse leverage **event sourcing principles**, where every state transition is represented by an operation. GraphQL input types describe operations, ensuring that user intents are captured effectively. These operations detail the parameters needed for state transitions. The use of GraphQL aligns these transitions with explicit, validated, and reproducible commands. ## The document model state schema First, let's add a GraphQL type that represents an individual item in a todo-list document. A todo item has an ID, text, and can be either checked or unchecked. Add this underneath the boilerplate `TodoListState` type you see in the Global State Schema text editor. ```graphql # Defines a GraphQL type for a single to-do item type TodoItem { id: OID! # Unique identifier for each to-do item text: String! # The text description of the to-do item checked: Boolean! # Status of the to-do item (checked/unchecked) } ``` Now update the `TodoListState` type to use our new type by replacing the boilerplate with this: ```graphql type TodoListState { items: [TodoItem!]! } ``` The final result in your editor should look like this: ```graphql type TodoListState { items: [TodoItem!]! } # Defines a GraphQL type for a single to-do item type TodoItem { id: OID! # Unique identifier for each to-do item text: String! # The text description of the to-do item checked: Boolean! # Status of the to-do item (checked/unchecked) } ``` With our state schema defined, go ahead and click the "Sync with schema" button underneath "Global State Initial Value". This will set the initial state for the documents you create with this model based on the schema you defined. Your initial value field should now look like this: ```json { "items": [] } ``` ## Operation inputs and their schemas We've defined the shape for the state of our `TodoList` documents, but we also need to be able to update them. Documents are updated by dispatching actions, which are applied to documents as operations. We define modules to group sets of operations together. In this simple case, we will only need one module. Add a new module for our `todos` operations by typing `todos` in the "Add new module" input and pressing enter. We need to add three different operations to this module: 1. add todo item 2. update todo item 3. delete todo item Let's start with adding todos. When we add a new todo, the only input we need to provide is the text. Creating the ID will be handled later in our reducer code, and todos always start as unchecked by default. type `add todo item` in the "Add new operation" input and press enter. You will see your new operation with the name `ADD_TODO_ITEM` (we automatically handle changing the casing to the required CONSTANT_CASE). You will also see a boilerplate placeholder GraphQL input. Update the GraphQL input like so: ```graphql input AddTodoItemInput { id: OID! text: String! } ``` Next let's handle updating todo items. Type `update todo item` in the "Add new operation" input and press enter. For updating items, we will need to provide an `id` so we know which one to update. We can use the same operation to update the text or the checked state, so both of these fields are optional (no ! on the field). Update the `UpdateTodoItemInput` to be like so: ```graphql input UpdateTodoItemInput { id: OID! text: String checked: Boolean } ``` Finally, we can handle the delete item operation. type `delete todo item` in the "Add new operation" input. For deleting items, all we need is an `id`. Update your `DeleteTodoItemInput` to be like this: ```graphql input DeleteTodoItemInput { id: OID! } ``` Once you have added all the input operations, click the `Export` button at the top right of the editor to save the document model specification document to your local machine. Ideally, you should save your file in the root of your repository with the name `todo-list.phd` ## Generating your document model code With our newly created document model, we can run the codegen to generate the rest of the code for it. To run the codegen, you use the `generate` command with a path to the file you just exported. ```bash pnpm generate ./todo-list.phd ``` **NOTE:** this generated code contains values that will always be different for each generated document model, like module ids for example. For the purposes of this tutorial, we recommend that you instead use the reference example that we have already included in the repo for you โ€” this will make your generated code look exactly the same as the generated code in the branches in the repo, and your diffs will match exactly. To use our reference example, run: ```bash pnpm generate ./todo-list.phd ``` This will overwrite your generated code with code that is identical to the branches in this repository. ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-1-complete-generated-todo-list-document-model` ### Up next: reducers and operations Up next, you'll learn how to implement the runtime logic and components that will use the `TodoList` document model specification you've just created and exported. --- ## Step 2 โ€” Implement the `TodoList` document model reducer operation handlers > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/ImplementTodoListDocumentModelReducerOperationHandlers ## Adding the logic for handling operations with reducers Your document model update's the state of a given document by applying a set of append-only actions. Once these have been applied to the document, we call them operations. The document model does this with a reducer โ€” a function which takes the existing state and a given action, and then returns the new state with the action applied. ## What we have so far The operation handler logic for each module is found in `document-models/SOME-DOCUMENT-MODEL/src/reducers/SOME-MODULE-NAME.ts`. So for our todos module, we will implement our handler logic in `document-models/todo-list/src/reducers/todos.ts` When you generated your document model code, we created a boilerplate implementation of the reducer logic for each of the operations we defined in step 1. You will see that there are functions for handling each of the operations, but all they do is throw "not implemented" errors. ```ts export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` Let's add the handler logic for each operation in the same order we defined them in the previous step. To handle the `addTodoItemOperation`, all we need to do is push a new todo item into the `state.items` array (which we defined in Step 1). When an action is dispatched (we will see this later), all the inputs are available in the `action.input` object. Update your `addTodoItemOperation` like so: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { // removed-start addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, // removed-end // added-start addTodoItemOperation(state, action) { state.items.push({ id: action.input.id, text: action.input.text, checked: false, }); }, // added-end updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` Under the hood, we use a library for making the functions always create and return new copies of the state, i.e. they are always _immutable_. This is why you don't actually have to return your new state, the newly created copy of the state is used automatically. The `updateTodoOperation` works in much the same way, except this time instead of creating a new `id`, we find the item in the items array which has the given id. Then we spread out the rest of the values we get from the action input, same as when creating. Update your `updateTodoOperation` to be like so: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { state.items.push({ id: action.input.id, text: action.input.text, checked: false, }); }, // removed-start updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, // removed-end // added-start updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return state; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // added-end deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` The delete operation is the simplest of the three. All we need to do is filter the items array so that it no longer contains the item with the given id. ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { state.items.push({ id: action.input.id, text: action.input.text, checked: false, }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return state; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // removed-start deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, // removed-end // added-start deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, // added-end }; ``` With that all done, your final result should look like this: ```ts export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { state.items.push({ id: action.input.id, text: action.input.text, checked: false, }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return state; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-2-complete-implemented-todo-list-document-model-reducer-operation-handlers` ### Up next: tests for our new operation handlers Up next, you'll implement some custom tests to check the behavior of our new code. --- ## Step 3 โ€” Adding our own tests for the document model actions > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/AddTestsForTodoListActions Similarly to the operation handler logic, when you add a new module to your document model, we generate some boilerplate tests for your code. Take a look in `document-models/todo-list/src/tests/todos.test.ts` You will see that we have some basic "sanity check" style tests for you already. These make sure that your operations are at least able to result in a valid document model state. You should copy these boilerplate checks in your other tests to ensure that your outputs are valid. ```ts /** * This is a scaffold file meant for customization: * - change it by adding new tests or modifying the existing ones */ reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { // the `createDocument` utility function from your document model creates // an a new empty document, i.e. one with your default initial state const document = utils.createDocument(); // the generate mock function takes one of your generated input schemas // and creates an object populated with random values for each field const input = generateMock(AddTodoItemInputSchema()); // we call your document model's reducer with the new document we just created // and the action we want to test, `addTodoItem` in this case // the reducer returns a new object, which is the document with the action applied // if successful, there will be an operation which corresponds to this action // in the updated document's operations list const updatedDocument = reducer(document, addTodoItem(input)); // when you generate a document model, we give you some validation utilities like // `isTodoListDocument` which confirms the document is of the correct form in a way // that typescript recognizes expect(isTodoListDocument(updatedDocument)).toBe(true); // at the start a document will have 0 operations, so after applying this action // there should now be one operation expect(updatedDocument.operations.global).toHaveLength(1); // the operation added to the list should correspond to the correct action type expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); // the operation added should have used the correct input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // the index of the operation should be 0, since it is the first and only operation expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation", () => { // ... }); it("should handle deleteTodoItem operation", () => { // ... }); }); ``` Since testing the `addTodoItemOperation` is such a simple case, we have not added further testing here. You are welcome to add a more test cases for it if you want. ## Tests for update operations ### Test updating the todo item text Let's add some more sophisticated tests for our `updateTodoItem` operation. We want to know that we can update todos successfully, and that we we do so it only changes the values we want to change, while leaving the rest as is. Delete the existing "should handle updateTodoItem operation" test. ```typescript // removed-start it("should handle updateTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(UpdateTodoItemInputSchema()); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); // removed-end ``` Let's test that the text of a todo item is updated correctly first. Put this code in the place where you just deleted the existing test case: ```ts it("should handle updateTodoItem operation to update text", () => { // we need there to already be a todo item in the document, // since we want to test updating an existing document const mockItem = generateMock(UpdateTodoItemInputSchema()); // we also need to generate a mock input for the update operation we are testing const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // since the mocks are generated with random values, we need to set the `id` on our mock input // to match the `id` of the existing mock input input.id = mockItem.id; // we want to easily check if the item's text was updated to be our new value, // so we assign a variable and use that for the mock input's text field const newText = "new text"; input.text = newText; // we are only testing updating the text here, so we want the checked field on the input // to be undefined, i.e. it should not change anything on the existing item input.checked = undefined; // we can pass a different initial state to the `createDocument` utility, // so in this case we pass in an `items` array with our existing item already in it const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); /* The following checks are copied from the boilerplate */ // create an updated document by applying the reducer with the action and input const updatedDocument = reducer(document, updateTodoItem(input)); // there should now be one operation in the operations list expect(updatedDocument.operations.global).toHaveLength(1); // the operation applied should correspond to an action of the correct type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // the operation applied should have used the correct input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // the operation applied should be the first operation in the list expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // find the updated item in the items list by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's text should now be updated to be our new text expect(updatedItem?.text).toBe(newText); // the item's `checked` field should be unchanged. expect(updatedItem?.checked).toBe(mockItem.checked); }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ### Test updating the todo item checked state Now let's do the same thing, but for the checked state of an item. This test is essentially just the same as the above, but we update the `checked` field while leaving the `text` field `undefined`. Add this code below the test case we just added: ```ts it("should handle updateTodoItem operation to update checked", () => { // generate a mock existing item const mockItem = generateMock(UpdateTodoItemInputSchema()); // generate a mock input const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // set the mock input's `id` to the mock item's `id` input.id = mockItem.id; // we want the new `checked` field value to be the opposite of the randomly generated value from the mock const newChecked = !mockItem.checked; input.checked = newChecked; // leave the `text` field unchanged input.text = undefined; // create a document with the existing item in it const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); // apply the reducer with the action and the mock input const updatedDocument = reducer(document, updateTodoItem(input)); /* The following checks are copied from the boilerplate */ // check your operations expect(updatedDocument.operations.global).toHaveLength(1); // check the operation's action type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // check the operation's input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // check the operation's index expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // get the updated item by it's `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's `text` field should remain unchanged expect(updatedItem?.text).toBe(mockItem.text); // the item's `checked` field should be updated to our new checked value expect(updatedItem?.checked).toBe(newChecked); }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ## Test for deleting todo items You will have seen that the tests for the `deleteTodoItem` operation passed, even though we didn't set up an existing item to delete. This is because the boilerplate just checks that the operation was applied with the correct inputs, which it technically was. Checking that it actually had the _result_ we want is our job. Update the `deleteTodoItem` operation test case to also create an existing item and then check that is was actually deleted: ```typescript it("should handle deleteTodoItem operation", () => { // generate a mock existing item const mockItem = generateMock(UpdateTodoItemInputSchema()); const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); const input = generateMock(DeleteTodoItemInputSchema()); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ## Final result After these updates, your `document-models/todo-list/src/tests/todos.test.ts` file should look like this: ```ts reducer, utils, isTodoListDocument, addTodoItem, updateTodoItem, deleteTodoItem, AddTodoItemInputSchema, UpdateTodoItemInputSchema, DeleteTodoItemInputSchema, type UpdateTodoItemInput, type TodoItem, } from "todo-tutorial/document-models/todo-list"; describe("TodosOperations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(UpdateTodoItemInputSchema()); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { // we need there to already be a todo item in the document, // since we want to test updating an existing document const mockItem = generateMock(UpdateTodoItemInputSchema()); // we also need to generate a mock input for the update operation we are testing const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); // since the mocks are generated with random values, we need to set the `id` on our mock input // to match the `id` of the existing mock input input.id = mockItem.id; // we want to easily check if the item's text was updated to be our new value, // so we assign a variable and use that for the mock input's text field const newText = "new text"; input.text = newText; // we are only testing updating the text here, so we want the checked field on the input // to be undefined, i.e. it should not change anything on the existing item input.checked = undefined; // we can pass a different initial state to the `createDocument` utility, // so in this case we pass in an `items` array with our existing item already in it const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); /* The following checks are copied from the boilerplate */ // create an updated document by applying the reducer with the action and input const updatedDocument = reducer(document, updateTodoItem(input)); // there should now be one operation in the operations list expect(updatedDocument.operations.global).toHaveLength(1); // the operation applied should correspond to an action of the correct type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // the operation applied should have used the correct input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // the operation applied should be the first operation in the list expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // find the updated item in the items list by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's text should now be updated to be our new text expect(updatedItem?.text).toBe(newText); // the item's `checked` field should be unchanged. expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { // generate a mock existing item const mockItem = generateMock(UpdateTodoItemInputSchema()); // generate a mock input const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); // set the mock input's `id` to the mock item's `id` input.id = mockItem.id; // we want the new `checked` field value to be the opposite of the randomly generated value from the mock const newChecked = !mockItem.checked; input.checked = newChecked; // leave the `text` field unchanged input.text = undefined; // create a document with the existing item in it const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); // apply the reducer with the action and the mock input const updatedDocument = reducer(document, updateTodoItem(input)); /* The following checks are copied from the boilerplate */ // check your operations expect(updatedDocument.operations.global).toHaveLength(1); // check the operation's action type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // check the operation's input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // check the operation's index expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // get the updated item by it's `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's `text` field should remain unchanged expect(updatedItem?.text).toBe(mockItem.text); // the item's `checked` field should be updated to our new checked value expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { // generate a mock existing item const mockItem = generateMock(UpdateTodoItemInputSchema()); const document = utils.createDocument({ global: { items: [mockItem as TodoItem], }, }); const input = generateMock(DeleteTodoItemInputSchema()); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-3-complete-implemented-tests-for-todo-operations` ### Up next: generating an editor for our `TodoList` documents Up next, we'll generate a boilerplate document editor for our `TodoList` documents. --- ## Step 4 โ€” Generating a document model editor for `TodoList` documents > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/GenerateTodoListDocumentEditor ## Generate the editor template Run the command below to generate the editor template for the `TodoList` document model. This command reads the `TodoList` document model definition from the `document-models` folder and generates the editor template in the `editors/todo-list-editor` folder. ```bash pnpm generate --editor TodoListEditor --document-types powerhouse/todo-list ``` Notice the `--editor` flag which specifies the editor name, and the `--document-types` flag defines the document type `powerhouse/todo-list`. Once complete, you'll have a new directory structure: ``` editors/todo-list-editor/ โ”œโ”€โ”€ components/ โ”‚ โ””โ”€โ”€ EditName.tsx # Auto-generated component for editing document name โ”œโ”€โ”€ editor.tsx # Main editor component (do not change this) โ””โ”€โ”€ module.ts # Editor module export (do not change this) ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to create a `TodoList` type document and open it. You will see the generic `EditName` component in the document - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-4-complete-generated-todo-list-document-editor` ## Up next: adding UI components for updating our `TodoList` documents Next, we will add some UI components to create, read, update, and delete data in our `TodoList` document editor. --- ## Step 5 โ€” Implement `TodoList` document editor UI components > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/ImplementTodoListDocumentEditorUIComponents Out of the box, we have a component for updating our `TodoList` documents' names, but we would like to create, read, update, and delete all of the data in our documents. ## Add a component for showing our todo list in the document editor Let's start by adding a `` component that will be the main container we show when you open a TodoList document. Create a new file at `editors/todo-list-editor/components/TodoList.tsx` and add this: ```jsx /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoListDocument] = useSelectedTodoListDocument(); if (!selectedTodoListDocument) return null; return (
Todo List Document
            {JSON.stringify(selectedTodoListDocument, null, 2)}
          
); } ``` ## Adding the Document Toolbar The `DocumentToolbar` component provides essential document operations like saving, sharing, and navigation. To add it to your document editor, simply import it from the design system and place it at the top of your editor component. The toolbar automatically connects to the currently selected document and provides all standard document actions. For more details, see the [DocumentToolbar documentation](../docs/../02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx). Now in `editors/todo-list-editor/editor.tsx` add this new component (TodoList) at the ```tsx // added-start // added-end export default function Editor() { ... return (
...
... // added-start // added-end
); } ``` Now when you open a TodoList document in Connect, you will see an (albeit ugly for now) representation of your whole document in JSON. ## Add a component for adding todo items to a todo list Next, let's add a component for adding todos to a todo list. Create a new file at `editors/todo-list-editor/components/AddTodo.tsx` and add this to it: ```jsx export function AddTodo() { // The hooks for getting documents also return a dispatch function for dispatching actions to modify the document. // This is the same pattern you will have seen in React's `useReducer` hook, except you don't need to pass the initial state. // The document we are working with _is_ the initial state. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const onSubmitAddTodo: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const addTodoInput = form.elements.namedItem("addTodo") as HTMLInputElement; const text = addTodoInput.value; if (!text) return; dispatch(addTodoItem({ text, id: generateId() })); form.reset(); }; return (
Add Todo
); } ``` We have provided some basic Tailwind styles but you are welcome to style your components however you wish. This hooks and functions also work with other component libraries like Radix etc. Let's add this component to our `` component. `./editors/todo-list-editor/components/TodoList.tsx` ```tsx /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoListDocument] = useSelectedTodoListDocument(); if (!selectedTodoListDocument) return null; return (
Todo List Document
            {JSON.stringify(selectedTodoListDocument, null, 2)}
          
); } ``` Now when you open a TodoList document in Connect, you can add more todos. ## Add components for todo items and the list of todo items Finally, let's add a component for showing and editing and individual todo item in a todo list, and another one for showing the list of todo items. Create a new file at `editors/todo-list-editor/components/Todo.tsx` and add this content: ```jsx useState, type ChangeEventHandler, type FormEventHandler, type MouseEventHandler, } from "react"; deleteTodoItem, updateTodoItem, } from "todo-tutorial/document-models/todo-list"; type Props = { todo: TodoItem; }; /** Displays a single todo item in the selected todo list * * Allows checking/unchecking the todo item. * Allows editing the todo item text. * Allows deleting the todo item. */ export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); // even though this component is for a todo item and not a whole list, we can use the exact same hook for dispatching updates to it. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const todoId = todo.id; const todoText = todo.text; const todoChecked = todo.checked; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; // we can use this dispatch function for any of the actions supported by a TodoList document dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch( updateTodoItem({ id: todo.id, checked: event.target.checked, }), ); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todoId })); }; const onClickEditTodo: MouseEventHandler = () => { setIsEditing(true); }; const onClickCancelEditTodo: MouseEventHandler = () => { setIsEditing(false); }; if (isEditing) return (
); return (
{todoText}
); } ``` Now create another new file at `editors/todo-list-document/Todos.tsx` and give it this content: ```jsx type Props = { todos: TodoItem[]; }; /** Shows a list of the todo items in the selected todo list */ export function Todos({ todos }: Props) { const hasTodos = todos.length > 0; return (
Todos
{!hasTodos ? (

Start adding things to your todo list

) : (
    {todos.map((todo) => (
  • ))}
)}
); } ``` And replace the content of your `TodoList.tsx` file with this: ```jsx /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoListDocument] = useSelectedTodoListDocument(); if (!selectedTodoListDocument) return null; const todos = selectedTodoListDocument.state.global.items; return (
); } ``` ``` editors/todo-list-editor/ โ”œโ”€โ”€ components/ โ”‚ โ””โ”€โ”€ AddTodo.tsx # Component for adding a new todo โ”‚ โ””โ”€โ”€ Todo.tsx # Renders an individual Todo item โ”‚ โ””โ”€โ”€ TodoList.tsx # main Todo Component, renders the Todos and AddTodo Component โ”‚ โ””โ”€โ”€ Todos.tsx # Renders the list of todos โ”œโ”€โ”€ editor.tsx # Main editor component โ””โ”€โ”€ module.ts # Editor module export (do not change this) ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to open a `TodoList` document and update all of the fields we defined in the `TodoList` document model schema - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-5-complete-added-basic-todo-list-document-editor-ui-components` ## Up next: generating a custom drive explorer for managing our `TodoList` documents Next, we will generate a special kind of editor called a "drive editor" which we will use instead of the generic drive explorer. --- ## Step 6 - Generate the custom drive editor for managing our `TodoList` documents > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/GenerateTodoDriveExplorer in Connect, a "drive" is just a document with the type "powerhouse/drive", which is specifically created for containing and managing other documents. A "drive editor" is just an editor that specifically works on documents with the type "powerhouse/document-drive". When you create a new drive in Connect, you are creating a new document of the "powerhouse/document-drive" type. So far, we have been working in a drive editor called "Generic Drive Explorer" which is the default drive editor and is designed to work with any document type. We can also create our own custom drive explorer and restrict it to only working on `TodoList` documents. This lets us add more special features than are possible when you need to support any type of document. ## Generating the `TodoDriveExplorer` drive editor To generate our `TodoDriveExplorer`, run the following command: ```bash pnpm generate --drive-editor TodoDriveApp --allowed-document-types powerhouse/todo-list ``` This will generate the following file structure: ```bash editors โ”‚ โ”œโ”€โ”€ todo-drive-explorer โ”‚ โ”‚ โ”œโ”€โ”€ components โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ CreateDocument.tsx # component for creating now documents โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DriveContents.tsx # component for showing the documents in the drive โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DriveExplorer.tsx # wrapper for the various other components โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ EmptyState.tsx # shown when there are no documents โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Files.tsx # shows a list of the file nodes (documents) in the drive โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Folders.tsx # shows a list of the folder nodes in the drive โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ FolderTree.tsx # shows the files and folders in the drive in a traditional sidebar format โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ NavigationBreadcrumbs.tsx # allows navigating the folders in a drive โ”‚ โ”‚ โ”œโ”€โ”€ config.ts # configuration for the drive including which document types are allowed โ”‚ โ”‚ โ”œโ”€โ”€ editor.tsx # main editor component (do not change this) โ”‚ โ”‚ โ””โ”€โ”€ module.ts # module export for the editor (do not change this) ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to create a new drive with the type "TodoDriveApp" and use it the same as you would the "Generic Drive Explorer", except it should only allow you to create `TodoList` type documents. - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-6-complete-generated-todo-drive-explorer` ## Up next: adding a shared component for showing stats about todos in both our `TodoListEditor` and `TodoDriveExplorer` Next, we will add a UI component that is useful in both of our different editors. --- ## Step 7 - Add shared component for showing TodoList stats > Source: https://powerhouse.academy/academy/ExampleUsecases/TodoList/AddSharedComponentForShowingTodoListStats So far we've been creating components that live in the same directories as the editors that use them. But sometimes we want to use the same component across multiple editors. Let's create a component for showing statistics about our todos. We'd like this component to work with any set of todos or todo lists, so that we can use the same one in our document editor or in our drive editor or a folder. ## Creating the `` component Create a new directory at `editors/components` and create two new files inside it: `editors/components/Stats.tsx` with this content: ```jsx TodoItem, TodoListDocument, } from "todo-tutorial/document-models/todo-list"; type Props = { todos: TodoItem[] | undefined; todoListDocuments?: TodoListDocument[] | undefined; createdAtUtcIso?: string; lastModifiedAtUtcIso?: string; }; /** Generic component for showing statistics about todo lists and the todos they contain */ export function Stats({ todos, todoListDocuments, createdAtUtcIso, lastModifiedAtUtcIso, }: Props) { const totalTodos = todos?.length ?? 0; const totalChecked = todos?.filter((todo) => todo.checked).length ?? 0; const totalUnchecked = todos?.filter((todo) => !todo.checked).length ?? 0; const percentageChecked = Math.round( calculatePercentage(totalTodos, totalChecked), ); const percentageUnchecked = Math.round( calculatePercentage(totalTodos, totalUnchecked), ); const createdAt = createdAtUtcIso ? new Date(createdAtUtcIso) : null; const hasCreatedAt = createdAt !== null; const lastModified = lastModifiedAtUtcIso ? new Date(lastModifiedAtUtcIso) : null; const hasLastModified = lastModified !== null; const createdAtFormattedDate = createdAt ? createdAt.toLocaleDateString() : null; const lastModifiedFormattedDate = lastModified ? lastModified.toLocaleDateString() : null; const createdAtFormattedTime = createdAt ? createdAt.toLocaleTimeString() : null; const lastModifiedFormattedTime = lastModified ? lastModified.toLocaleTimeString() : null; const totalTodoListDocuments = todoListDocuments?.length ?? 0; const hasTodoLists = todoListDocuments !== undefined; return (
Statistics
    {hasTodoLists && (
  • Todo Lists {totalTodoListDocuments}
  • )}
  • Todos {totalTodos}
  • Checked {totalChecked} ({percentageChecked}%)
  • Unchecked {totalUnchecked} ({percentageUnchecked}%)
  • {hasCreatedAt && (
  • Created {createdAtFormattedDate} {createdAtFormattedTime}
  • )} {hasLastModified && (
  • Last modified {lastModifiedFormattedDate} {lastModifiedFormattedTime}
  • )}
); } function calculatePercentage(total: unknown, value: unknown) { if (typeof total !== "number" || typeof value !== "number") { return 0; } const ratio = value / total; if (isNaN(ratio)) { return 0; } return ratio * 100; } ``` And `editors/components/index.ts` with this content: ```ts export { Stats } from "./Stats.js"; ``` The index file lets us use a nice neat import path like `todo-tutorial/editors/components` in all of our editor components. Don't be too concerned with the math and time related code you see here โ€” those are just implementation details. ## Using the `` component in our `TodoListEditor` Now let's use the `` component in our `` component `editors/todo-list-editor/components/TodoList.tsx`: ```tsx // added-start // added-end /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoListDocument] = useSelectedTodoListDocument(); if (!selectedTodoListDocument) return null; const todos = selectedTodoListDocument.state.global.items; // added-start const createdAtUtcIso = selectedTodoListDocument.header.createdAtUtcIso; const lastModifiedAtUtcIso = selectedTodoListDocument.header.lastModifiedAtUtcIso; // added-end return (
// added-start // added-end
); } ``` With this, you will now see statistics about the todo items in a todo list document. And now we can also show off the flexibility of our new `` component. Since drives are also just documents themselves, we can derive the same information about a drive too. This means we can use this same component in our drive editor as well. ## Using the `` component in our `TodoDriveExplorer` Let's add this to our `` component `editors/todo-drive-app/components/DriveContents.tsx`, along with some conditional logic that either shows stats for the selected folder (if one is selected) or the selected drive otherwise. ```tsx // added-start useSelectedDrive, useSelectedFolder, } from "@powerhousedao/reactor-browser"; // added-end // added-start useTodoListDocumentsInSelectedDrive, useTodoListDocumentsInSelectedFolder, type TodoItem, type TodoListDocument, } from "todo-tutorial/document-models/todo-list"; /** Small helper function to get all todo items from all todo lists */ export function getAllTodoItemsFromTodoLists( todoLists: TodoListDocument[] | undefined, ): TodoItem[] { return todoLists?.flatMap((todoList) => todoList.state.global.items) ?? []; } // added-end /** Shows the documents and folders in the selected drive */ export function DriveContents() { // added-start const selectedFolder = useSelectedFolder(); const hasSelectedFolder = selectedFolder !== undefined; // added-end return (
// added-line {hasSelectedFolder ? : }
); } // added-start /** Shows the statistics for the selected drive */ function DriveStats() { const todoListDocumentsInSelectedDrive = useTodoListDocumentsInSelectedDrive(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedDrive, ); const [selectedDrive] = useSelectedDrive(); const driveCreatedAt = selectedDrive.header.createdAtUtcIso; const driveLastModified = selectedDrive.header.lastModifiedAtUtcIso; return ( ); } /** Shows the statistics for the selected folder */ function FolderStats() { const todoListDocumentsInSelectedFolder = useTodoListDocumentsInSelectedFolder(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedFolder, ); return ( ); } // added-end ``` The final result should look like this: ```jsx useSelectedDrive, useSelectedFolder, } from "@powerhousedao/reactor-browser"; useTodoListDocumentsInSelectedDrive, useTodoListDocumentsInSelectedFolder, type TodoItem, type TodoListDocument, } from "todo-tutorial/document-models/todo-list"; /** Small helper function to get all todo items from all todo lists */ export function getAllTodoItemsFromTodoLists( todoLists: TodoListDocument[] | undefined, ): TodoItem[] { return todoLists?.flatMap((todoList) => todoList.state.global.items) ?? []; } /** Shows the documents and folders in the selected drive */ export function DriveContents() { const selectedFolder = useSelectedFolder(); const hasSelectedFolder = selectedFolder !== undefined; return (
{hasSelectedFolder ? : }
); } /** Shows the statistics for the selected drive */ function DriveStats() { const todoListDocumentsInSelectedDrive = useTodoListDocumentsInSelectedDrive(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedDrive, ); const [selectedDrive] = useSelectedDrive(); const driveCreatedAt = selectedDrive.header.createdAtUtcIso; const driveLastModified = selectedDrive.header.lastModifiedAtUtcIso; return ( ); } /** Shows the statistics for the selected folder */ function FolderStats() { const todoListDocumentsInSelectedFolder = useTodoListDocumentsInSelectedFolder(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedFolder, ); return ( ); } ``` With this update, you can now see the statistics for the todo lists and todo items for the selected drive, folder or document depending on which you select. ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to see the `` component showing the data for your drives, folder and documents. - make sure your code matches the code in the completed step branch run: `git diff step-7-complete-added-shared-component-for-showing-todo-list-stats` ## The end Congratulations! You now have a working `TodoList` document model, and editor for those documents, and a drive editor for managing those documents. This will make a good starting point for creating your own new implementations. We're excited to see what you build! --- ## VetraPackageLibrary > Source: https://powerhouse.academy/academy/ExampleUsecases/VetraPackageLibrary/VetraPackageLibrary Packages of documents are a core structuring mechanism in the Vetra framework, allowing developers to group and manage related document efficiently. These packages serve as modular collections of document specifications and modules, ensuring consistency, scalability, and reusability across different applications. ### Example packages - **Finance Package** โ€“ A set of documents or apps handling invoices, payments, budgets, and financial reporting. - **Contributor Billing Package** โ€“ Defines documents for tracking work, invoicing, and facilitating payments (in both fiat and crypto) for contributors in decentralized organizations. - **Governance Package** โ€“ Models for proposals, voting, contributor agreements, and decision-making processes. - **People Ops Package** โ€“ Documents managing contributor profiles, roles, task assignments, and reputation tracking. - **Project Management Package** โ€“ Models for task tracking, milestones, resource allocation, and deliverables. --- The Vetra package library is your best source to explore packages built by different builder teams on the Powerhouse Vetra platform. Through the packages you'll find the GitHub repository with the source code of each package. You can get access to the [package library here.](https://vetra.io/packages) --- # API References ## Powerhouse CLI > Source: https://powerhouse.academy/academy/APIReferences/PowerhouseCLI ### Installing the Powerhouse CLI **TIP:** The **Powerhouse CLI tool** is the only essential tool to install on this page. Install it with the command below. You can find all of the commands on this page, similar to what would displayed when using ph --help or ph _command_ --help. Use the table of content or the search function to find what you are looking for. The Powerhouse CLI (`ph-cmd`) is a command-line interface tool that provides essential commands for managing Powerhouse Vetra projects. You can get access to the Powerhouse ecosystem tools by installing them globally. ```bash pnpm install -g ph-cmd ``` {/* AUTO-GENERATED-CLI-COMMANDS-START */} {/* This content is automatically generated. Do not edit directly. */} ## Quick Reference | Command | Description | Example | |---------|-------------|---------| | `ph init` | Initialize a new project | `ph init my-project --pnpm` | | `ph use` | Switch to a release version | `ph use staging` | | `ph update` | Update dependencies to latest | `ph update` | | `ph setup-globals` | Initialize global project | `ph setup-globals my-globals` | | `ph use-local` | Use local monorepo dependencies | `ph use-local ../powerhouse` | --- ### ph-cmd Commands - [Init](#init) - [Use](#use) - [Update](#update) - [Setup Globals](#setup-globals) - [Use Local](#use-local) ## Init Initialize a new project --- ## Parameters ### Arguments **Name** - The name of your project. A new directory will be created in your current directory with this name. - Usage: `[name]` ### Options **Name** - The name of your project. A new directory will be created in your current directory with this name. - Usage: `--name, -n ` **Package Manager** - Specify the package manager to use for your project. Can be one of: `npm`, `pnpm`, `yarn`, or `bun`. Defaults to your environment package manager. - Usage: `--package-manager, -p ` **Tag** - Specify the release tag to use for your project. Can be one of: "latest", "staging", or "dev". - Usage: `--tag, -t ` **Version** - Specify the exact semver release version to use for your project. - Usage: `--version, -v ` **Remote Drive** - Remote drive identifier. - Usage: `--remote-drive, -r ` ### Flags **Npm** - Use 'npm' as package manager - Usage: `--npm` **Pnpm** - Use 'pnpm' as package manager - Usage: `--pnpm` **Yarn** - Use 'yarn' as package manager - Usage: `--yarn` **Bun** - Use 'bun' as package manager - Usage: `--bun` **Dev** - Use the `dev` release tag. - Usage: `--dev, -d` **Staging** - Use the `staging` release tag. - Usage: `--staging, -s` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Use Specify the release version of Powerhouse dependencies to use. --- ## Parameters ### Arguments **Tag** - Specify the release tag to use for your project. Can be one of: "latest", "staging", or "dev". - Usage: `[tag]` ### Options **Tag** - Specify the release tag to use for your project. Can be one of: "latest", "staging", or "dev". - Usage: `--tag, -t ` **Version** - Specify the exact semver release version to use for your project. - Usage: `--version, -v ` ### Flags **Skip Install** - Skip running `install` with your package manager - Usage: `--skip-install, -s` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Update Update your powerhouse dependencies to their latest tagged version ### Flags **Skip Install** - Skip running `install` with your package manager - Usage: `--skip-install, -s` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Setup Globals Initialize a new global project --- ## Parameters ### Arguments **Name** - The name of your project. A new directory will be created in your current directory with this name. - Usage: `[name]` ### Options **Name** - The name of your project. A new directory will be created in your current directory with this name. - Usage: `--name, -n ` **Package Manager** - Specify the package manager to use for your project. Can be one of: `npm`, `pnpm`, `yarn`, or `bun`. Defaults to your environment package manager. - Usage: `--package-manager, -p ` **Tag** - Specify the release tag to use for your project. Can be one of: "latest", "staging", or "dev". - Usage: `--tag, -t ` **Version** - Specify the exact semver release version to use for your project. - Usage: `--version, -v ` **Remote Drive** - Remote drive identifier. - Usage: `--remote-drive, -r ` ### Flags **Npm** - Use 'npm' as package manager - Usage: `--npm` **Pnpm** - Use 'pnpm' as package manager - Usage: `--pnpm` **Yarn** - Use 'yarn' as package manager - Usage: `--yarn` **Bun** - Use 'bun' as package manager - Usage: `--bun` **Dev** - Use the `dev` release tag. - Usage: `--dev, -d` **Staging** - Use the `staging` release tag. - Usage: `--staging, -s` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Use Local Use your local `powerhouse` monorepo dependencies the current project. --- ## Parameters ### Arguments **Monorepo Path** - Path to your local powerhouse monorepo relative to this project - Usage: `[monorepo path]` ### Options **Path** - Path to your local powerhouse monorepo relative to this project - Usage: `--path, -p ` ### Flags **Skip Install** - Skip running `install` with `pnpm` - Usage: `--skip-install, -s` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ### ph-cli Commands - [Generate](#generate) - [All](#all) - [Document Model](#document-model) - [Editor](#editor) - [App](#app) - [Processor](#processor) - [Subgraph](#subgraph) - [Migration File](#migration-file) - [Vetra](#vetra) - [Connect](#connect) - [Connect Studio](#connect-studio) - [Connect Build](#connect-build) - [Connect Preview](#connect-preview) - [Access Token](#access-token) - [Inspect](#inspect) - [List](#list) - [Migrate](#migrate) - [Switchboard](#switchboard) - [Login](#login) - [Install](#install) - [Uninstall](#uninstall) ## Generate The generate command creates code for Powerhouse modules. It helps you create new code from scratch, or to re-generate existing code in your project. ## All Re-generate all modules in the current project ### Flags **Help** - show help - Usage: `--help, -h` ## Document Model Generate a document model ### Options **File** - Path to the file to generate the document model from - Usage: `--file, -f ` **Dir** - Name of the directory of an existing document model to re-generate - Usage: `--dir, -d ` ### Flags **All** - Re-generate all existing document models in the current project - Usage: `--all, -a` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Editor Generate a document editor ### Options **Name** - The name of the document editor to generate - Usage: `--name, -n ` **Document Type** - The document type for the new editor - Usage: `--document-type, -t ` **Dir** - Name of the directory of an existing editor to re-generate - Usage: `--dir, -d ` ### Flags **All** - Re-generate all existing editors in the current project - Usage: `--all, -a` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## App Generate a drive app ### Options **Name** - The name of the drive app to generate - Usage: `--name, -n ` **Document Types** - The document types allowed by the new app - Usage: `--document-types , -t=` **Dir** - Name of the directory of an existing app to re-generate - Usage: `--dir, -d ` ### Flags **Disable Drag And Drop** - Do not allow drag and drop in this drive app. - Usage: `--disable-drag-and-drop` **Default:** `false` **All** - Re-generate all existing apps in the current project - Usage: `--all, -a` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Processor Generate a processor ### Options **Name** - The name of the processor to generate - Usage: `--name, -n ` **Type** - The type of processor to generate - Usage: `--type ` **Default:** `analytics` **Document Types** - The document types the processor will run on - Usage: `--document-types , -t=` **Default:** `` **Apps** - Whether the processor will run in switchboard (nodejs), connect (browser), or both - Usage: `--apps ` **Default:** `switchboard,connect` **Dir** - Name of the directory of an existing processor to re-generate - Usage: `--dir, -d ` ### Flags **All** - Re-generate all existing processors in the current project - Usage: `--all, -a` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Subgraph Generate a subgraph ### Options **Name** - The name of the subgraph to generate - Usage: `--name, -n ` **Dir** - Name of the directory of an existing subgraph to re-generate - Usage: `--dir, -d ` ### Flags **All** - Re-generate all existing subgraphs in the current project - Usage: `--all, -a` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Migration File Generate a migration file ### Options **Path *[required]*** - Path to the migration file - Usage: `--path, -p ` **Schema File** - Path to the output file. Defaults to './schema.ts' - Usage: `--schema-file ` ### Flags **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Vetra The vetra command sets up a Vetra development environment for working with Vetra projects. It starts a Vetra Switchboard and optionally Connect Studio, enabling document collaboration and real-time processing with a "Vetra" drive or connection to remote drives. **What it does:** - 1. Starts a Vetra Switchboard with a "Vetra" drive for document storage - 2. Optionally connects to remote drives instead of creating a local drive - 3. Starts Connect Studio pointing to the Switchboard for user interaction (unless disabled) - 4. Enables real-time updates, collaboration, and code generation ### Options **Switchboard Port** - port to use for the Vetra Switchboard - Usage: `--switchboard-port ` **Connect Port** - port to use for the Vetra Connect - Usage: `--connect-port ` **Default:** `3001` **Remote Drive** - URL of remote drive to connect to (skips switchboard initialization) - Usage: `--remote-drive ` **Base** - Base path for the app - Usage: `--base ` **Environment:** `PH_CONNECT_BASE_PATH` **Log Level** - Log level for the application - Usage: `--log-level ` **Environment:** `PH_CONNECT_LOG_LEVEL` **Default:** `info` **Packages** - Comma-separated list of package names to load - Usage: `--packages ` **Environment:** `PH_PACKAGES` **Local Package** - Path to local package to load during development - Usage: `--local-package ` **Environment:** `PH_LOCAL_PACKAGE` **Default Drives Url** - The default drives url to use in connect - Usage: `--default-drives-url ` **Environment:** `PH_CONNECT_DEFAULT_DRIVES_URL` **Drive Preserve Strategy** - The preservation strategy to use on default drives - Usage: `--drive-preserve-strategy ` **Environment:** `PH_CONNECT_DRIVES_PRESERVE_STRATEGY` **Default:** `preserve-by-url-and-detach` **Host** - Expose the server to the network. Pass an IP (e.g. 0.0.0.0) to bind to a specific address. - Usage: `--host ` **Watch Timeout** - Amount of time to wait before a file is considered changed - Usage: `--watch-timeout ` **Environment:** `PH_WATCH_TIMEOUT` **Default:** `300` **Https Key File** - path to the ssl key file - Usage: `--https-key-file ` **Https Cert File** - path to the ssl cert file - Usage: `--https-cert-file ` **Remote Drives** - Specify remote drive URLs to use - Usage: `--remote-drives ` ### Flags **Watch** - Enable dynamic loading for document-models and editors in connect-studio and switchboard - Usage: `--watch, -w` **Default:** `false` **Logs** - Show additional logs - Usage: `--logs` **Default:** `false` **Disable Connect** - Skip Connect initialization (only start switchboard and reactor) - Usage: `--disable-connect` **Default:** `false` **Interactive** - Enable interactive mode for code generation (requires user confirmation before generating code) - Usage: `--interactive` **Default:** `false` **Ignore Local** - Do not load local packages from this project - Usage: `--ignore-local` **Environment:** `PH_DISABLE_LOCAL_PACKAGE` **Force** - Force dep pre-optimization regardless of whether deps have changed. - Usage: `--force` **Debug** - Log arguments passed to this command - Usage: `--debug` **Open** - Open browser on startup - Usage: `--open` **Cors** - Enable CORS - Usage: `--cors` **Strict Port** - Exit if specified port is already in use - Usage: `--strictPort` **Print Urls** - Print server urls - Usage: `--print-urls` **Default:** `true` **Bind Cli Shortcuts** - Bind CLI shortcuts - Usage: `--bind-cli-shortcuts` **Default:** `true` **Https** - Use https - Usage: `--https` **Dev** - enable development mode to load local packages - Usage: `--dev` **Help** - show help - Usage: `--help, -h` ## Connect Powerhouse Connect commands. Use with `studio`, `build` or `preview`. Defaults to `studio` if not specified. ## Connect Studio The studio command starts the Connect Studio, a development environment for building and testing Powerhouse applications. It provides a visual interface for working with your project. **What it does:** - 1. Starts a local Connect Studio server - 2. Provides a web interface for development - 3. Allows you to interact with your project components - 4. Supports various configuration options for customization ### Options **Port** - Port to run the dev server on. - Usage: `--port ` **Default:** `3000` **Base** - Base path for the app - Usage: `--base ` **Environment:** `PH_CONNECT_BASE_PATH` **Log Level** - Log level for the application - Usage: `--log-level ` **Environment:** `PH_CONNECT_LOG_LEVEL` **Default:** `info` **Packages** - Comma-separated list of package names to load - Usage: `--packages ` **Environment:** `PH_PACKAGES` **Local Package** - Path to local package to load during development - Usage: `--local-package ` **Environment:** `PH_LOCAL_PACKAGE` **Default Drives Url** - The default drives url to use in connect - Usage: `--default-drives-url ` **Environment:** `PH_CONNECT_DEFAULT_DRIVES_URL` **Drive Preserve Strategy** - The preservation strategy to use on default drives - Usage: `--drive-preserve-strategy ` **Environment:** `PH_CONNECT_DRIVES_PRESERVE_STRATEGY` **Default:** `preserve-by-url-and-detach` **Host** - Expose the server to the network. Pass an IP (e.g. 0.0.0.0) to bind to a specific address. - Usage: `--host ` **Watch Timeout** - Amount of time to wait before a file is considered changed - Usage: `--watch-timeout ` **Environment:** `PH_WATCH_TIMEOUT` **Default:** `300` ### Flags **Ignore Local** - Do not load local packages from this project - Usage: `--ignore-local` **Environment:** `PH_DISABLE_LOCAL_PACKAGE` **Force** - Force dep pre-optimization regardless of whether deps have changed. - Usage: `--force` **Debug** - Log arguments passed to this command - Usage: `--debug` **Open** - Open browser on startup - Usage: `--open` **Cors** - Enable CORS - Usage: `--cors` **Strict Port** - Exit if specified port is already in use - Usage: `--strictPort` **Print Urls** - Print server urls - Usage: `--print-urls` **Default:** `true` **Bind Cli Shortcuts** - Bind CLI shortcuts - Usage: `--bind-cli-shortcuts` **Default:** `true` **Help** - show help - Usage: `--help, -h` ## Connect Build The Connect build command creates a production build with the project's local and external packages included ### Options **Out Dir** - Output directory - Usage: `--outDir ` **Default:** `.ph/connect-build/dist/` **Base** - Base path for the app - Usage: `--base ` **Environment:** `PH_CONNECT_BASE_PATH` **Log Level** - Log level for the application - Usage: `--log-level ` **Environment:** `PH_CONNECT_LOG_LEVEL` **Default:** `info` **Packages** - Comma-separated list of package names to load - Usage: `--packages ` **Environment:** `PH_PACKAGES` **Local Package** - Path to local package to load during development - Usage: `--local-package ` **Environment:** `PH_LOCAL_PACKAGE` **Default Drives Url** - The default drives url to use in connect - Usage: `--default-drives-url ` **Environment:** `PH_CONNECT_DEFAULT_DRIVES_URL` **Drive Preserve Strategy** - The preservation strategy to use on default drives - Usage: `--drive-preserve-strategy ` **Environment:** `PH_CONNECT_DRIVES_PRESERVE_STRATEGY` **Default:** `preserve-by-url-and-detach` ### Flags **Ignore Local** - Do not load local packages from this project - Usage: `--ignore-local` **Environment:** `PH_DISABLE_LOCAL_PACKAGE` **Force** - Force dep pre-optimization regardless of whether deps have changed. - Usage: `--force` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Connect Preview The Connect preview command previews a built Connect project. NOTE: You must run `ph connect build` first ### Options **Port** - Port to run the preview server on. - Usage: `--port ` **Default:** `4173` **Out Dir** - Output directory - Usage: `--outDir ` **Default:** `.ph/connect-build/dist/` **Base** - Base path for the app - Usage: `--base ` **Environment:** `PH_CONNECT_BASE_PATH` **Log Level** - Log level for the application - Usage: `--log-level ` **Environment:** `PH_CONNECT_LOG_LEVEL` **Default:** `info` **Packages** - Comma-separated list of package names to load - Usage: `--packages ` **Environment:** `PH_PACKAGES` **Local Package** - Path to local package to load during development - Usage: `--local-package ` **Environment:** `PH_LOCAL_PACKAGE` **Default Drives Url** - The default drives url to use in connect - Usage: `--default-drives-url ` **Environment:** `PH_CONNECT_DEFAULT_DRIVES_URL` **Drive Preserve Strategy** - The preservation strategy to use on default drives - Usage: `--drive-preserve-strategy ` **Environment:** `PH_CONNECT_DRIVES_PRESERVE_STRATEGY` **Default:** `preserve-by-url-and-detach` **Host** - Expose the server to the network. Pass an IP (e.g. 0.0.0.0) to bind to a specific address. - Usage: `--host ` **Watch Timeout** - Amount of time to wait before a file is considered changed - Usage: `--watch-timeout ` **Environment:** `PH_WATCH_TIMEOUT` **Default:** `300` ### Flags **Ignore Local** - Do not load local packages from this project - Usage: `--ignore-local` **Environment:** `PH_DISABLE_LOCAL_PACKAGE` **Force** - Force dep pre-optimization regardless of whether deps have changed. - Usage: `--force` **Debug** - Log arguments passed to this command - Usage: `--debug` **Open** - Open browser on startup - Usage: `--open` **Cors** - Enable CORS - Usage: `--cors` **Strict Port** - Exit if specified port is already in use - Usage: `--strictPort` **Print Urls** - Print server urls - Usage: `--print-urls` **Default:** `true` **Bind Cli Shortcuts** - Bind CLI shortcuts - Usage: `--bind-cli-shortcuts` **Default:** `true` **Help** - show help - Usage: `--help, -h` ## Access Token The access-token command generates a bearer token for API authentication. This token can be used to authenticate requests to Powerhouse APIs like reactor-api (Switchboard). **What it does:** - 1. Uses your CLI's cryptographic identity (DID) to sign a verifiable credential - 2. Creates a JWT bearer token with configurable expiration - 3. Outputs the token to stdout (info to stderr) for easy piping Prerequisites: You must have a cryptographic identity. Run 'ph login' first to: - Generate a keypair (stored in .ph/.keypair.json) - Optionally link your Ethereum address (stored in .ph/.renown.json) Token Details: The generated token is a JWT (JSON Web Token) containing: - Issuer (iss): Your CLI's DID (did:key:...) - Subject (sub): Your CLI's DID - Credential Subject: Chain ID, network ID, and address (if authenticated) - Expiration (exp): Based on --expiry option - Audience (aud): If --audience is specified Output: - Token information (DID, address, expiry) is printed to stderr - The token itself is printed to stdout for easy piping/copying This allows you to use the command in scripts: TOKEN=$(ph access-token) curl -H "Authorization: Bearer $TOKEN" http://localhost:4001/graphql Usage with APIs: Generate token and use with curl TOKEN=$(ph access-token --expiry 1d) curl -X POST http://localhost:4001/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '\{"query": "\{ drives \{ id name \} \}"\}' Export as environment variable export PH_ACCESS_TOKEN=$(ph access-token) Notes: - Tokens are self-signed using your CLI's private key - No network request is made; tokens are generated locally - The recipient API must trust your CLI's DID to accept the token - For reactor-api, ensure AUTH_ENABLED=true to require authentication ### Options **Expiry** - Token expiry duration. Supports: "7d" (days), "24h" (hours), "3600" or "3600s" (seconds) - Usage: `--expiry ` **Default:** `7d` **Audience** - Target audience URL for the token - Usage: `--audience ` ### Flags **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Inspect The inspect command examines and provides detailed information about a Powerhouse package. It helps you understand the structure, dependencies, and configuration of packages in your project. **What it does:** - 1. Analyzes the specified package - 2. Retrieves detailed information about its structure and configuration - 3. Displays package metadata, dependencies, and other relevant information - 4. Helps troubleshoot package-related issues --- ## Parameters ### Arguments **Package Name *[required]*** - The name of the package to inspect - Usage: `` ### Flags **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## List The list command displays information about installed Powerhouse packages in your project. It reads the powerhouse.config.json file and shows the packages that are currently installed. **What it does:** - 1. Examines your project configuration - 2. Lists all installed Powerhouse packages - 3. Provides a clear overview of your project's dependencies - 4. Helps you manage and track your Powerhouse components ### Flags **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Migrate Run migrations --- ## Parameters ### Arguments **Version** - The version to migrate to. Accepts a valid semver version or `staging`, `dev`, `latest`. - Usage: `[version]` ### Options **Version** - The version to migrate to. Accepts a valid semver version or `staging`, `dev`, `latest`. - Usage: `--version, -v ` **Default:** `latest` ### Flags **Force** - Run migrate from the bundled codegen even if the target version cannot be resolved from the npm registry or differs from the installed ph-cli version. - Usage: `--force, -f` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Switchboard The switchboard command starts a local Switchboard instance, which acts as the document processing engine for Powerhouse projects. It provides the infrastructure for document models, processors, and real-time updates. **What it does:** - 1. Starts a local switchboard server - 2. Loads document models and processors - 3. Provides an API for document operations - 4. Enables real-time document processing - 5. Can authenticate with remote services using your identity from 'ph login' ### Flags **Https** - Use https - Usage: `--https` **Dev** - enable development mode to load local packages - Usage: `--dev` **Ignore Local** - Do not load local packages from this project - Usage: `--ignore-local` **Environment:** `PH_DISABLE_LOCAL_PACKAGE` **Debug** - Log arguments passed to this command - Usage: `--debug` **Use Identity** - enable identity using keypair from ph login (uses ~/.ph/keypair.json) - Usage: `--use-identity` **Require Identity** - require existing keypair, fail if not found (implies --use-identity) - Usage: `--require-identity` **Migrate** - Run database migrations and exit - Usage: `--migrate` **Migrate Status** - Show migration status and exit - Usage: `--migrate-status` **Mcp** - enable Mcp route at /mcp - Usage: `--mcp` **Default:** `true` **Use Vetra Drive** - Use a Vetra drive - Usage: `--use-vetra-drive` **Default:** `false` **Help** - show help - Usage: `--help, -h` ### Options **Https Key File** - path to the ssl key file - Usage: `--https-key-file ` **Https Cert File** - path to the ssl cert file - Usage: `--https-cert-file ` **Remote Drives** - Specify remote drive URLs to use - Usage: `--remote-drives ` **Packages** - Comma-separated list of package names to load - Usage: `--packages ` **Environment:** `PH_PACKAGES` **Port** - Port to host the api - Usage: `--port ` **Default:** `4001` **Base Path** - base path for the API endpoints (sets the BASE_PATH environment variable) - Usage: `--base-path ` **Keypair Path** - path to custom keypair file for identity - Usage: `--keypair-path ` **Vetra Drive Id** - Specify a Vetra drive ID - Usage: `--vetra-drive-id ` **Default:** `vetra` **Db Path** - path to the database - Usage: `--db-path ` ## Login The login command authenticates you with Renown using your Ethereum wallet. This enables the CLI to act on behalf of your Ethereum identity for authenticated operations. **What it does:** - 1. Generates or loads a cryptographic identity (DID) for the CLI - 2. Opens your browser to the Renown authentication page - 3. You authorize the CLI's DID to act on behalf of your Ethereum address - 4. Stores the credentials locally in .ph/.renown.json ### Options **Renown Url** - Renown server URL. - Usage: `--renown-url ` **Environment:** `PH_CONNECT_RENOWN_URL` **Default:** `https//www.renown.id` **Timeout** - Authentication timeout in seconds. - Usage: `--timeout ` **Default:** `300` ### Flags **Logout** - Sign out and clear stored credentials - Usage: `--logout` **Status** - Show current authentication status - Usage: `--status` **Show Did** - Show the CLI's DID and exit - Usage: `--show-did` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Install The install command adds Powerhouse dependencies to your project. By default it only registers the package in powerhouse.config.json with provider "registry" โ€” Connect will load it from the registry CDN at runtime. With --local, the package is also installed into node_modules and marked as provider "local" โ€” it will be bundled into ph connect build so the preview works without the registry being reachable. Resolution order for the registry URL: --registry flag > PH_REGISTRY_URL env > powerhouse.config.json > default --- ## Parameters ### Arguments **Dependencies *[required]*** - Names of the dependencies to install - Usage: `[...dependencies]` ### Options **Registry** - Registry URL to install from (overrides config and environment) - Usage: `--registry ` **Allow Build** - A list of package names that are allowed to run postinstall scripts during installation. - Usage: `--allow-build ` **Package Manager** - Specify the package manager to use for your project. Can be one of: `npm`, `pnpm`, `yarn`, or `bun`. Defaults to your environment package manager. - Usage: `--package-manager, -p ` ### Flags **Local** - Also install packages into node_modules (marks them as provider: "local" so they get bundled into ph connect build) - Usage: `--local` **Npm** - Use 'npm' as package manager - Usage: `--npm` **Pnpm** - Use 'pnpm' as package manager - Usage: `--pnpm` **Yarn** - Use 'yarn' as package manager - Usage: `--yarn` **Bun** - Use 'bun' as package manager - Usage: `--bun` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` ## Uninstall The uninstall command removes Powerhouse dependencies from your project. It handles the removal of packages, updates configuration files, and ensures proper cleanup. **What it does:** - 1. Uninstalls specified Powerhouse dependencies using your package manager - 2. Updates powerhouse.config.json to remove the dependencies - 3. Supports various uninstallation options and configurations - 4. Works with npm, yarn, yarn@berry, pnpm, pnpm@6, bun, deno package managers --- ## Parameters ### Arguments **Dependencies *[required]*** - Names of the dependencies to uninstall - Usage: `[...dependencies]` ### Options **Package Manager** - Specify the package manager to use for your project. Can be one of: `npm`, `pnpm`, `yarn`, or `bun`. Defaults to your environment package manager. - Usage: `--package-manager, -p ` ### Flags **Npm** - Use 'npm' as package manager - Usage: `--npm` **Pnpm** - Use 'pnpm' as package manager - Usage: `--pnpm` **Yarn** - Use 'yarn' as package manager - Usage: `--yarn` **Bun** - Use 'bun' as package manager - Usage: `--bun` **Debug** - Log arguments passed to this command - Usage: `--debug` **Help** - show help - Usage: `--help, -h` {/* AUTO-GENERATED-CLI-COMMANDS-END */} --- ## React Hooks > Source: https://powerhouse.academy/academy/APIReferences/ReactHooks This page provides a reference for the hooks available in `@powerhousedao/reactor-browser`. These hooks are intended to be used by editors (including drive editors) which will be rendered inside Powerhouse host-applications such as Connect, Switchboard, Fusion or a Vetra Studio Drive. - Learn more about [Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) - Learn more about [Drive-apps](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer)
Need a refresher on React Hooks? React Hooks allow you to use various React features directly within your functional components. You can use built-in Hooks or combine them to create your own custom Hooks. **What are Custom Hooks?** A custom hook is a JavaScript function whose name starts with "use" and that calls other Hooks. They are used to: - Reuse stateful logic between components. - Abstract complex logic into a simpler interface. - Isolate side effects, particularly those managed by `useEffect`. **Key Built-in Hooks Examples:** - `useState`: Lets a component "remember" information (state). - `useEffect`: Lets a component perform side effects (e.g., data fetching, subscriptions, manually changing the DOM). - `useContext`: Lets a component receive information from distant parent components without explicitly passing props through every level of the component tree. **Naming Convention:** Hook names must always start with `use` followed by a capital letter (e.g., `useState`, `useOnlineStatus`). **Rules of Hooks:** 1. **Only Call Hooks at the Top Level**: Don't call Hooks inside loops, conditions, or nested functions. 2. **Only Call Hooks from React Functions**: Call Hooks from React functional components or from custom Hooks. It's important to note that a function should only be named and treated as a hook if it actually utilizes one or more built-in React hooks. If a function (even if named `useSomething`) doesn't call any built-in hooks, it behaves like a regular JavaScript function, and making it a "hook" offers no specific React advantages.
## Key Concepts ### Reactor All of the data used by these hooks is ultimately derived from the `Reactor`, which manages the asynchronous eventually consistent state of drives and documents. Learn more about the [Reactor](/academy/Architecture/WorkingWithTheReactor). ### Dispatch Function Many hooks return a `dispatch` function for modifying documents. The dispatch function has this signature: ```typescript function dispatch( actionOrActions: Action | Action[] | undefined, onErrors?: (errors: Error[]) => void, ): void; ``` **Parameters:** - `actionOrActions` โ€” The action or array of actions to dispatch to the document - `onErrors` โ€” Optional callback invoked with any errors that occurred during action execution --- ## Quick Reference | Category | Hooks | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Selected Document** | `useSelectedDocument`, `useSelectedDocumentSafe`, `useSelectedDocumentId`, `useSelectedDocumentOfType` | | **Document by ID** | `useDocumentById`, `useDocumentsByIds`, `useDocumentOfType` | | **Document Cache** | `useDocumentCache`, `useDocument`, `useDocuments`, `useGetDocument`, `useGetDocuments`, `useGetDocumentAsync` | | **Drives** | `useDrives`, `useSelectedDrive`, `useSelectedDriveSafe`, `useSelectedDriveId` | | **Nodes & Folders** | `useSelectedNode`, `useSelectedFolder`, `useNodeById`, `useNodePathById` | | **Items in Drive** | `useNodesInSelectedDrive`, `useFileNodesInSelectedDrive`, `useFolderNodesInSelectedDrive`, `useDocumentsInSelectedDrive` | | **Items in Folder** | `useNodesInSelectedFolder`, `useFileNodesInSelectedFolder`, `useFolderNodesInSelectedFolder`, `useDocumentsInSelectedFolder` | | **Node Actions** | `useNodeActions` | | **Modals** | `usePHModal`, `showPHModal`, `closePHModal`, `showCreateDocumentModal`, `showDeleteNodeModal` | | **Revision History** | `useRevisionHistoryVisible`, `showRevisionHistory`, `hideRevisionHistory` | | **Timeline** | `useSelectedTimelineItem`, `useSelectedTimelineRevision` | | **Config** | `useAllowedDocumentTypes`, `useIsDragAndDropEnabled`, `useIsExternalControlsEnabled` | --- ## Selected Document ### `useSelectedDocumentId` Returns the ID of the currently selected document. ```typescript check function useSelectedDocumentId(): string | undefined; ``` **Returns:** The selected document's ID, or `undefined` if no file node is selected. --- ### `useSelectedDocument` Returns the selected document along with a dispatch function. Throws error if no document selected. ```typescript function useSelectedDocument(): readonly [ PHDocument, ( actionOrActions: Action | Action[] | undefined, onErrors?: (errors: Error[]) => void, ) => void, ]; ``` **Returns:** A tuple `[document, dispatch]` where: - `document` โ€” The selected document - `dispatch` โ€” A function to dispatch actions to the document **Example:** ```tsx function DocumentViewer() { const [document, dispatch] = useSelectedDocument(); if (!document) { return

No document selected

; } return (

{document.name}

Type: {document.header.documentType}

); } ``` **See also:** [`useSelectedDocumentSafe`](#useselecteddocumentsafe), [`useSelectedDocumentOfType`](#useselecteddocumentoftype), [`useDocumentById`](#usedocumentbyid) --- ### `useSelectedDocumentSafe` Returns the selected document along with a dispatch function or undefined is no document is selected. ```typescript function useSelectedDocumentSafe(): readonly [ PHDocument | undefined, ( actionOrActions: Action | Action[] | undefined, onErrors?: (errors: Error[]) => void, ) => void, ]; ``` **Returns:** A tuple `[document, dispatch]` where: - `document` โ€” The selected document, or `undefined` if none selected - `dispatch` โ€” A function to dispatch actions to the document **Throws:** - `NoSelectedDocumentError` โ€” When no document is selected **Example:** ```tsx function DocumentViewer() { const [document, dispatch] = useSelectedDocument(); if (!document) { return

No document selected

; } return (

{document.name}

Type: {document.header.documentType}

); } ``` **See also:** [`useSelectedDocumentOfType`](#useselecteddocumentoftype), [`useDocumentById`](#usedocumentbyid) --- ### `useSelectedDocumentOfType` Returns the selected document of a specific type along with a dispatch function. Throws an error if the found document has a different type. ```typescript function useSelectedDocumentOfType< TDocument extends PHDocument, TAction extends Action, >(documentType: string): [TDocument, DocumentDispatch]; function useSelectedDocumentOfType(documentType: null | undefined): never[]; ``` **Parameters:** - `documentType` โ€” The expected document type string (e.g., `"powerhouse/budget-statement"`) **Returns:** A tuple `[document, dispatch]` with the document typed as `TDocument`. **Throws:** - `NoSelectedDocumentError` โ€” When no document is selected - `DocumentTypeMismatchError` - When selected document has different document type than the one provided **Example:** ```tsx function BudgetEditor() { const [document, dispatch] = useSelectedDocumentOfType< BudgetStatementDocument, BudgetStatementAction >("powerhouse/budget-statement"); const handleUpdate = () => { dispatch({ type: "UPDATE_BUDGET", input: { amount: 1000 } }, (errors) => console.error("Failed:", errors), ); }; return
{/* editor UI */}
; } ``` --- ## Document by ID ### `useDocumentById` Returns a document by ID along with a dispatch function. ```typescript function useDocumentById( id: string | null | undefined, ): readonly [ PHDocument | undefined, ( actionOrActions: Action | Action[] | undefined, onErrors?: (errors: Error[]) => void, ) => void, ]; ``` **Parameters:** - `id` โ€” The document ID to retrieve, or `null`/`undefined` to skip retrieval **Returns:** A tuple `[document, dispatch]` where: - `document` โ€” The document if found, or `undefined` - `dispatch` โ€” A function to dispatch actions to the document **Example:** ```tsx function DocumentCard({ documentId }: { documentId: string }) { const [document, dispatch] = useDocumentById(documentId); if (!document) { return

Loading...

; } return
{document.name}
; } ``` --- ### `useDocumentsByIds` Returns multiple documents by their IDs. ```typescript function useDocumentsByIds(ids: string[] | null | undefined): PHDocument[]; ``` **Parameters:** - `ids` โ€” Array of document IDs to retrieve, or `null`/`undefined` to skip **Returns:** An array of documents. Returns an empty array if `ids` is `null`/`undefined`. --- ### `useDocumentOfType` Returns a document of a specific type. Throws an error if the document has a different type. ```typescript function useDocumentOfType< TDocument extends PHDocument, TAction extends Action, >( documentId: string | null | undefined, documentType: string | null | undefined, ): [TDocument, DocumentDispatch] | never[]; ``` **Parameters:** - `documentId` โ€” The document ID to retrieve - `documentType` โ€” The expected document type **Throws:** - `DocumentNotFoundError` โ€” When the document doesn't exist - `DocumentModelNotFoundError` โ€” When the document model isn't registered - `DocumentTypeMismatchError` โ€” When the document type doesn't match --- ## Document Cache ### `useDocumentCache` Returns the document cache containing all documents in the reactor. ```typescript function useDocumentCache(): IDocumentCache | undefined; ``` --- ### `useDocument` Retrieves a document from the reactor and subscribes to changes using React Suspense. This hook will suspend rendering while the document is loading. ```typescript function useDocument(id: string | null | undefined): PHDocument | undefined; ``` **Parameters:** - `id` โ€” The document ID to retrieve, or `null`/`undefined` to skip retrieval **Returns:** The document if found, or `undefined` if `id` is `null`/`undefined`. --- ### `useDocuments` Retrieves multiple documents from the reactor using React Suspense. This hook will suspend rendering while any of the documents are loading. ```typescript function useDocuments(ids: string[] | null | undefined): PHDocument[]; ``` **Parameters:** - `ids` โ€” Array of document IDs to retrieve, or `null`/`undefined` to skip retrieval **Returns:** An array of documents. Returns an empty array if `ids` is `null`/`undefined`. --- ### `useGetDocument` Returns a function to retrieve a document from the cache. The returned function fetches and returns a document by ID. ```typescript function useGetDocument(): (id: string) => Promise; ``` **Returns:** A function that takes a document ID and returns a Promise of the document. **Example:** ```tsx function DocumentFetcher() { const getDocument = useGetDocument(); const handleFetch = async (id: string) => { const document = await getDocument(id); console.log("Fetched document:", document.name); }; return ; } ``` --- ### `useGetDocuments` Returns a function to retrieve multiple documents from the cache. The returned function fetches and returns documents by their IDs. ```typescript function useGetDocuments(): (ids: string[]) => Promise; ``` **Returns:** A function that takes an array of document IDs and returns a Promise of the documents. --- ### `useGetDocumentAsync` Retrieves a document from the reactor without suspending rendering. Returns the current state of the document loading operation. ```typescript function useGetDocumentAsync(id: string | null | undefined): { status: "initial" | "pending" | "success" | "error"; data: PHDocument | undefined; isPending: boolean; error: Error | undefined; reload: (() => Promise) | undefined; }; ``` **Parameters:** - `id` โ€” The document ID to retrieve, or `null`/`undefined` to skip retrieval **Returns:** An object containing: - `status` โ€” `"initial"` | `"pending"` | `"success"` | `"error"` - `data` โ€” The document if successfully loaded - `isPending` โ€” Boolean indicating if the document is currently loading - `error` โ€” Any error that occurred during loading - `reload` โ€” Function to force reload the document from cache **Example:** ```tsx function AsyncDocumentLoader({ id }: { id: string }) { const { status, data, isPending, error, reload } = useGetDocumentAsync(id); if (status === "initial" || isPending) { return

Loading...

; } if (status === "error") { return (

Error: {error?.message}

); } return
{data?.name}
; } ``` --- ## Drives ### `useDrives` Returns all drives in the reactor. ```typescript function useDrives(): DocumentDriveDocument[] | undefined; ``` **Example:** ```tsx function DriveList() { const drives = useDrives(); return (
    {drives?.map((drive) => (
  • {drive.header.slug}
  • ))}
); } ``` --- ### `useSelectedDriveId` Returns the ID of the currently selected drive. ```typescript function useSelectedDriveId(): string | undefined; ``` --- ### `useSelectedDrive` Returns the selected drive along with a dispatch function. **Throws an error if no drive is selected.** ```typescript function useSelectedDrive(): [ DocumentDriveDocument, DocumentDispatch, ]; ``` **Returns:** A tuple `[drive, dispatch]`. **Throws:** `Error` with message `"There is no drive selected. Did you mean to call 'useSelectedDriveSafe'?"` **See also:** [`useSelectedDriveSafe`](#useselecteddrivesafe) --- ### `useSelectedDriveSafe` Returns the selected drive, or `undefined` if no drive is selected. Use this when you need to handle the "no drive selected" case gracefully. ```typescript function useSelectedDriveSafe(): | [DocumentDriveDocument, DocumentDispatch] | readonly [undefined, undefined]; ``` **Returns:** A tuple `[drive, dispatch]` or `[undefined, undefined]` if no drive is selected. **Example:** ```tsx function DriveHeader() { const [drive, dispatch] = useSelectedDriveSafe(); if (!drive) { return

Select a drive to get started

; } return

{drive.header.slug}

; } ``` --- ### `setSelectedDrive` Sets the selected drive and updates the URL. ```typescript function setSelectedDrive( driveOrDriveSlug: string | DocumentDriveDocument | undefined, ): void; ``` **Parameters:** - `driveOrDriveSlug` โ€” The drive object, drive slug string, or `undefined` to deselect --- ## Selected Node & Folder ### `useSelectedNode` Returns the currently selected node (file or folder). ```typescript function useSelectedNode(): Node | undefined; ``` --- ### `setSelectedNode` Sets the selected node and updates the URL. ```typescript function setSelectedNode(nodeOrNodeSlug: Node | string | undefined): void; ``` **Parameters:** - `nodeOrNodeSlug` โ€” The node object, node slug string, or `undefined` to deselect --- ### `useSelectedFolder` Returns the selected folder. Returns `undefined` if the selected node is not a folder. ```typescript function useSelectedFolder(): FolderNode | undefined; ``` --- ### `useNodeById` Returns a node in the selected drive by ID. ```typescript function useNodeById(id: string | null | undefined): Node | undefined; ``` **Parameters:** - `id` โ€” The node ID to find --- ### `useNodePathById` Returns the path (array of ancestor nodes) to a node in the selected drive. ```typescript function useNodePathById(id: string | null | undefined): Node[]; ``` **Parameters:** - `id` โ€” The node ID to get the path for **Returns:** An array of nodes from root to the target node. Returns an empty array if the node is not found. --- ### `useSelectedNodePath` Returns the path to the currently selected node. ```typescript function useSelectedNodePath(): Node[]; ``` --- ## Items in Selected Drive ### `useNodesInSelectedDrive` Returns all nodes (files and folders) in the selected drive. ```typescript function useNodesInSelectedDrive(): Node[] | undefined; ``` --- ### `useFileNodesInSelectedDrive` Returns only the file nodes in the selected drive. ```typescript function useFileNodesInSelectedDrive(): FileNode[] | undefined; ``` --- ### `useFolderNodesInSelectedDrive` Returns only the folder nodes in the selected drive. ```typescript function useFolderNodesInSelectedDrive(): FolderNode[] | undefined; ``` --- ### `useDocumentsInSelectedDrive` Returns all documents in the selected drive. ```typescript function useDocumentsInSelectedDrive(): PHDocument[] | undefined; ``` --- ### `useDocumentTypesInSelectedDrive` Returns the document types supported by the selected drive, as defined by the document model documents present in the drive. ```typescript function useDocumentTypesInSelectedDrive(): string[] | undefined; ``` --- ### `useNodesInSelectedDriveOrFolder` Returns the child nodes for the selected drive or folder. If a folder is selected, returns its children. Otherwise, returns the root-level nodes of the drive. ```typescript function useNodesInSelectedDriveOrFolder(): Node[]; ``` **Returns:** An array of nodes, sorted by name. Returns an empty array if no drive is selected. --- ## Items in Selected Folder ### `useNodesInSelectedFolder` Returns all nodes in the selected folder. ```typescript function useNodesInSelectedFolder(): Node[] | undefined; ``` --- ### `useFileNodesInSelectedFolder` Returns only the file nodes in the selected folder. ```typescript function useFileNodesInSelectedFolder(): FileNode[] | undefined; ``` --- ### `useFolderNodesInSelectedFolder` Returns only the folder nodes in the selected folder. ```typescript function useFolderNodesInSelectedFolder(): FolderNode[] | undefined; ``` --- ### `useDocumentsInSelectedFolder` Returns the documents in the selected folder. ```typescript function useDocumentsInSelectedFolder(): PHDocument[] | undefined; ``` --- ## Node Actions ### `useNodeActions` Returns a set of functions for performing file and folder operations in the selected drive. ```typescript function useNodeActions(): { onAddFile: ( file: File, parent: Node | undefined, ) => Promise; onAddFolder: ( name: string, parent: Node | undefined, ) => Promise; onRenameNode: (newName: string, node: Node) => Promise; onCopyNode: (src: Node, target: Node | undefined) => Promise; onMoveNode: (src: Node, target: Node | undefined) => Promise; onDuplicateNode: (src: Node) => Promise; onAddAndSelectNewFolder: (name: string) => Promise; }; ``` **Returned Functions:** | Function | Description | | ------------------------------- | ---------------------------------------------------------- | | `onAddFile(file, parent)` | Adds a file to the drive under the specified parent folder | | `onAddFolder(name, parent)` | Creates a new folder under the specified parent | | `onRenameNode(newName, node)` | Renames a node | | `onCopyNode(src, target)` | Copies a node to a target folder | | `onMoveNode(src, target)` | Moves a node to a target folder | | `onDuplicateNode(src)` | Duplicates a node in the current folder | | `onAddAndSelectNewFolder(name)` | Creates a new folder and selects it | **Example:** ```tsx useNodeActions, useSelectedFolder, } from "@powerhousedao/reactor-browser"; function FileUploader() { const { onAddFile, onAddFolder } = useNodeActions(); const selectedFolder = useSelectedFolder(); const handleFileUpload = async ( event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; if (file) { await onAddFile(file, selectedFolder); } }; const handleCreateFolder = async () => { await onAddFolder("New Folder", selectedFolder); }; return (
); } ``` --- ## Modals ### `usePHModal` Returns the currently displayed modal. ```typescript function usePHModal(): PHModal | undefined; ``` **Modal Types:** ```typescript type PHModal = | { type: "createDocument"; documentType: string } | { type: "deleteItem"; id: string } | { type: "addDrive" } | { type: "upgradeDrive"; driveId: string } | { type: "deleteDrive"; driveId: string } | { type: "driveSettings"; driveId: string } | { type: "settings" } | { type: "clearStorage" } | { type: "debugSettings" } | { type: "disclaimer" } | { type: "cookiesPolicy" } | { type: "exportDocumentWithErrors"; documentId: string } | { type: "inspector" }; ``` --- ### `showPHModal` Shows a modal. ```typescript function showPHModal(modal: PHModal): void; ``` --- ### `closePHModal` Closes the currently displayed modal. ```typescript function closePHModal(): void; ``` --- ### `showCreateDocumentModal` Shows the create document modal for a specific document type. ```typescript function showCreateDocumentModal(documentType: string): void; ``` **Example:** ```tsx function CreateButton() { return ( ); } ``` --- ### `showDeleteNodeModal` Shows the delete confirmation modal for a node. ```typescript function showDeleteNodeModal(nodeOrId: Node | string): void; ``` **Parameters:** - `nodeOrId` โ€” The node object or node ID to delete --- ## Revision History ### `useRevisionHistoryVisible` Returns whether the revision history panel is visible. ```typescript check function useRevisionHistoryVisible(): boolean | undefined; ``` --- ### `showRevisionHistory` Shows the revision history panel. ```typescript function showRevisionHistory(): void; ``` --- ### `hideRevisionHistory` Hides the revision history panel. ```typescript function hideRevisionHistory(): void; ``` --- ## Timeline ### `useSelectedTimelineItem` Returns the selected timeline item. ```typescript function useSelectedTimelineItem(): TimelineItem | null | undefined; ``` **Timeline Item Types:** ```typescript type TimelineBarItem = { id: string; type: "bar"; addSize?: 0 | 1 | 2 | 3 | 4; delSize?: 0 | 1 | 2 | 3 | 4; timestampUtcMs?: string; additions?: number; deletions?: number; revision?: number; startDate?: Date; endDate?: Date; }; type TimelineDividerItem = { id: string; type: "divider"; timestampUtcMs?: string; title?: string; subtitle?: string; revision?: number; startDate?: Date; endDate?: Date; }; type TimelineItem = TimelineBarItem | TimelineDividerItem; ``` --- ### `setSelectedTimelineItem` Sets the selected timeline item. ```typescript function setSelectedTimelineItem(item: TimelineItem | null | undefined): void; ``` --- ### `useSelectedTimelineRevision` Returns the selected timeline revision. ```typescript function useSelectedTimelineRevision(): string | number | null | undefined; ``` --- ### `setSelectedTimelineRevision` Sets the selected timeline revision. ```typescript function setSelectedTimelineRevision( revision: string | number | null | undefined, ): void; ``` --- ## Document Types ### `useDocumentTypes` Returns the document types a drive editor supports. Uses `allowedDocumentTypes` config if set, otherwise falls back to all supported document types from the reactor. ```typescript function useDocumentTypes(): string[] | undefined; ``` --- ### `useSupportedDocumentTypesInReactor` Returns the supported document types for the reactor, derived from the registered document model modules. ```typescript function useSupportedDocumentTypesInReactor(): string[] | undefined; ``` --- ## Vetra Packages ### `useVetraPackages` Returns all Vetra packages loaded by the Connect instance. ```typescript function useVetraPackages(): VetraPackage[] | undefined; ``` **VetraPackage Type:** ```typescript type VetraPackage = { id: string; name: string; description: string; category: string; author: Author; modules: { documentModelModules?: VetraDocumentModelModule[]; editorModules?: VetraEditorModule[]; subgraphModules?: SubgraphModule[]; importScriptModules?: ImportScriptModule[]; processorModules?: VetraProcessorModule[]; }; }; ``` --- ### `setVetraPackages` Sets the Vetra packages for the Connect instance. ```typescript function setVetraPackages(vetraPackages: VetraPackage[] | undefined): void; ``` --- ## Switchboard Link ### `useGetSwitchboardLink` Hook that returns a function to generate a document's switchboard URL. Only returns a function for documents in remote drives. Returns `null` for local drives or when the document/drive cannot be determined. The returned function generates a fresh bearer token and builds the switchboard URL with authentication when called. ```typescript function useGetSwitchboardLink( document: PHDocument | undefined, ): (() => Promise) | null; ``` **Parameters:** - `document` โ€” The document to create a switchboard URL generator for **Returns:** An async function that returns the switchboard URL, or `null` if not applicable. **Example:** ```tsx useGetSwitchboardLink, useSelectedDocument, } from "@powerhousedao/reactor-browser"; function SwitchboardButton() { const [document] = useSelectedDocument(); const getSwitchboardLink = useGetSwitchboardLink(document); if (!getSwitchboardLink) { return null; // Not available for local drives } const handleClick = async () => { const url = await getSwitchboardLink(); window.open(url, "_blank"); }; return ; } ``` --- ## Editor Configuration ### `useIsExternalControlsEnabled` Gets whether external controls are enabled for a given editor. ```typescript function useIsExternalControlsEnabled(): boolean | undefined; ``` --- ### `setIsExternalControlsEnabled` Sets whether external controls are enabled for a given editor. ```typescript function setIsExternalControlsEnabled(enabled: boolean | undefined): void; ``` --- ### `useIsDragAndDropEnabled` Gets whether drag and drop is enabled for a given drive editor. ```typescript check function useIsDragAndDropEnabled(): boolean | undefined; ``` --- ### `setIsDragAndDropEnabled` Sets whether drag and drop is enabled for a given drive editor. ```typescript function setIsDragAndDropEnabled(enabled: boolean | undefined): void; ``` --- ### `useAllowedDocumentTypes` Defines the document types a drive supports. Defaults to all document types registered in the reactor. ```typescript function useAllowedDocumentTypes(): string[] | undefined; ``` --- ### `setAllowedDocumentTypes` Sets the allowed document types for a given drive editor. ```typescript function setAllowedDocumentTypes(types: string[] | undefined): void; ``` --- ## Config: Set by Object ### `setPHDriveEditorConfig` Sets the global drive editor config. Pass in a partial object of the config to set. ```typescript function setPHDriveEditorConfig(config: Partial): void; ``` **Config Options:** - `allowedDocumentTypes` โ€” Array of allowed document type strings - `isDragAndDropEnabled` โ€” Whether drag and drop is enabled --- ### `setPHDocumentEditorConfig` Sets the global document editor config. Pass in a partial object of the config to set. ```typescript function setPHDocumentEditorConfig( config: Partial, ): void; ``` **Config Options:** - `isExternalControlsEnabled` โ€” Whether external controls are enabled --- ### `useSetPHDriveEditorConfig` Wrapper hook that automatically sets the global drive editor config when the component mounts. ```typescript function useSetPHDriveEditorConfig(config: Partial): void; ``` **Example:** ```tsx function MyDriveEditor() { useSetPHDriveEditorConfig({ isDragAndDropEnabled: true, allowedDocumentTypes: ["powerhouse/budget-statement", "powerhouse/invoice"], }); return
{/* editor content */}
; } ``` --- ### `useSetPHDocumentEditorConfig` Wrapper hook that automatically sets the global document editor config when the component mounts. ```typescript function useSetPHDocumentEditorConfig( config: Partial, ): void; ``` --- ## Config: Get by Key ### `usePHDriveEditorConfigByKey` Gets the value of an item in the global drive config for a given key. Strongly typed, inferred from type definition for the key. ```typescript function usePHDriveEditorConfigByKey( key: TKey, ): PHDriveEditorConfig[TKey]; ``` --- ### `usePHDocumentEditorConfigByKey` Gets the value of an item in the global document config for a given key. Strongly typed, inferred from type definition for the key. ```typescript function usePHDocumentEditorConfigByKey( key: TKey, ): PHDocumentEditorConfig[TKey]; ``` --- ## IReactorClient > Source: https://powerhouse.academy/academy/APIReferences/ReactorClient The `IReactorClient` interface is the primary way to interact with a Powerhouse reactor programmatically. It wraps lower-level APIs to provide a simpler, Promise-based interface for document operations. ```typescript ``` **INFO:** `@powerhousedao/reactor-browser` re-exports all reactor types for convenience in browser environments (editors, drive-apps, subgraphs). If you are working outside the browser โ€” for example in a standalone Node.js script, CLI tool, or server-side processor โ€” import directly from `@powerhousedao/reactor`. For an architectural overview of the reactor, see [Working with the Reactor](/academy/Architecture/WorkingWithTheReactor). For the low-level `IReactor` interface and access to internal components, see [Advanced Reactor Usage](/academy/APIReferences/AdvancedReactorUsage). ## Common parameter types Several types appear across multiple methods. They are described here once. ### `ViewFilter` Targets a specific branch, scopes, or revision when reading documents. ```typescript type ViewFilter = { branch?: string; scopes?: string[]; revision?: number; }; ``` | Field | Description | | ---------- | ----------------------------------------------- | | `branch` | The branch to read from (e.g. `"main"`) | | `scopes` | Scopes to include (e.g. `["global"]`) | | `revision` | Read the document at a specific revision number | ### `SearchFilter` Narrows which documents a query returns. ```typescript type SearchFilter = { type?: string; parentId?: string; ids?: string[]; slugs?: string[]; }; ``` ### `PagingOptions` Controls pagination for list methods. ```typescript type PagingOptions = { cursor: string; limit: number; }; ``` ### `PagedResults` Returned by all list methods. Includes a `next()` helper for fetching the next page. ```typescript type PagedResults = { results: T[]; options: PagingOptions; next?: () => Promise>; nextCursor?: string; totalCount?: number; }; ``` ### `PropagationMode` Controls how deletions handle child documents. ```typescript enum PropagationMode { None = "none", // Only delete the specified document Cascade = "cascade", // Also delete all child documents } ``` ### `CreateDocumentOptions` Options for `createEmpty()`. ```typescript type CreateDocumentOptions = { parentIdentifier?: string; // id or slug of parent document documentModelVersion?: number; // defaults to latest }; ``` ### `JobInfo` Tracks the status and result of a mutation job. ```typescript type JobInfo = { id: string; status: JobStatus; createdAtUtcIso: string; completedAtUtcIso?: string; error?: ErrorInfo; consistencyToken: ConsistencyToken; meta: JobMeta; }; ``` See [Job lifecycle](/academy/Architecture/WorkingWithTheReactor#job-lifecycle) for details on `JobStatus` values. --- ## Pagination best practices All list methods (`find`, `getOutgoingRelationships`, `getIncomingRelationships`, `getOperations`, `getDocumentModelModules`) accept `PagingOptions` and return `PagedResults`. Here are some guidelines for working with paginated results effectively. **Use `next()` for sequential iteration.** The `next()` helper on `PagedResults` handles cursor management for you: ```typescript let page = await reactorClient.find({ type: "powerhouse/todo-list" }); while (page) { for (const doc of page.results) { console.log(doc.header.id); } page = page.next ? await page.next() : undefined; } ``` **Set a reasonable `limit`.** The default page size varies by method. If you know you only need a few results, set a small limit to reduce response size: ```typescript const topFive = await reactorClient.getOutgoingRelationships( driveId, "child", undefined, { cursor: "", limit: 5, }, ); ``` **Check `nextCursor` to know if more pages exist.** When `nextCursor` is `undefined`, you have reached the end: ```typescript const page = await reactorClient.getOperations(docId); if (page.nextCursor) { // There are more operations to fetch } ``` **Avoid fetching all pages in tight loops for large datasets.** If you are processing thousands of documents or operations, consider processing each page before fetching the next to keep memory usage predictable. --- ## Cancellation with AbortSignal Most `IReactorClient` methods accept an optional `AbortSignal` parameter. This lets you cancel in-flight requests โ€” useful for cleaning up when a component unmounts, a user navigates away, or a timeout is reached. **Cancel on component unmount (React):** ```typescript useEffect(() => { const controller = new AbortController(); reactorClient .find( { type: "powerhouse/todo-list" }, undefined, undefined, controller.signal, ) .then(setResults) .catch((err) => { if (err.name !== "AbortError") throw err; }); return () => controller.abort(); }, []); ``` **Cancel with a timeout:** ```typescript const result = await reactorClient.get( docId, undefined, AbortSignal.timeout(5000), ); ``` **Cancel long-running writes.** Write methods like `execute()` wait for the job to reach `READ_READY`. If this takes too long, an abort signal lets you bail out: ```typescript const controller = new AbortController(); // Set a 10-second deadline setTimeout(() => controller.abort(), 10_000); const updated = await reactorClient.execute( docId, "main", actions, controller.signal, ); ``` When a request is aborted, the method throws an `AbortError`. The underlying reactor job may still complete โ€” aborting only cancels the client-side wait, not the server-side processing. --- ## Read methods ### `get` Retrieve a single document by id or slug. ```typescript get( identifier: string, view?: ViewFilter, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------ | ------------- | -------- | ---------------------------------- | | `identifier` | `string` | Yes | Document id or slug | | `view` | `ViewFilter` | No | Branch, scopes, or revision filter | | `signal` | `AbortSignal` | No | Cancel the request | **Example:** ```typescript const doc = await reactorClient.get("my-todo-list"); const atRevision = await reactorClient.get("my-todo-list", { revision: 5 }); ``` --- ### `getOutgoingRelationships` List documents that the given source has outgoing relationships to, filtered by relationship type. For example, pass `"child"` to list a parent's children. ```typescript getOutgoingRelationships( sourceIdentifier: string, relationshipType: string, view?: ViewFilter, paging?: PagingOptions, signal?: AbortSignal, ): Promise> ``` **Parameters:** | Name | Type | Required | Description | | ------------------ | --------------- | -------- | ----------------------------------------------- | | `sourceIdentifier` | `string` | Yes | Source document id or slug | | `relationshipType` | `string` | Yes | Relationship type to filter by (e.g. `"child"`) | | `view` | `ViewFilter` | No | Branch/scopes filter | | `paging` | `PagingOptions` | No | Pagination cursor and limit | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `getIncomingRelationships` List documents that have incoming relationships pointing at the given target, filtered by relationship type. For example, pass `"child"` to list a document's parents. ```typescript getIncomingRelationships( targetIdentifier: string, relationshipType: string, view?: ViewFilter, paging?: PagingOptions, signal?: AbortSignal, ): Promise> ``` **Parameters:** | Name | Type | Required | Description | | ------------------ | --------------- | -------- | ----------------------------------------------- | | `targetIdentifier` | `string` | Yes | Target document id or slug | | `relationshipType` | `string` | Yes | Relationship type to filter by (e.g. `"child"`) | | `view` | `ViewFilter` | No | Branch/scopes filter | | `paging` | `PagingOptions` | No | Pagination cursor and limit | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `find` Search for documents matching criteria. ```typescript find( search: SearchFilter, view?: ViewFilter, paging?: PagingOptions, signal?: AbortSignal, ): Promise> ``` **Parameters:** | Name | Type | Required | Description | | -------- | --------------- | -------- | --------------------------------------- | | `search` | `SearchFilter` | Yes | Filter by type, parentId, ids, or slugs | | `view` | `ViewFilter` | No | Branch/scopes filter | | `paging` | `PagingOptions` | No | Pagination cursor and limit | | `signal` | `AbortSignal` | No | Cancel the request | **Example:** ```typescript const todoLists = await reactorClient.find({ type: "powerhouse/todo-list", parentId: driveId, }); for (const doc of todoLists.results) { console.log(doc.header.id, doc.header.name); } ``` --- ### `getOperations` Retrieve the operation history of a document. ```typescript getOperations( documentIdentifier: string, view?: ViewFilter, filter?: OperationFilter, paging?: PagingOptions, signal?: AbortSignal, ): Promise> ``` **Parameters:** | Name | Type | Required | Description | | -------------------- | ----------------- | -------- | ----------------------------------------------- | | `documentIdentifier` | `string` | Yes | Document id or slug | | `view` | `ViewFilter` | No | Branch/scopes filter | | `filter` | `OperationFilter` | No | Filter by action types, timestamps, or revision | | `paging` | `PagingOptions` | No | Pagination cursor and limit | | `signal` | `AbortSignal` | No | Cancel the request | **`OperationFilter`:** ```typescript interface OperationFilter { actionTypes?: string[]; // e.g. ["ADD_TODO_ITEM"] timestampFrom?: string; // ISO string timestampTo?: string; // ISO string sinceRevision?: number; // operations with index >= this value } ``` --- ### `getDocumentModelModules` List registered document model modules. ```typescript getDocumentModelModules( namespace?: string, paging?: PagingOptions, signal?: AbortSignal, ): Promise> ``` **Parameters:** | Name | Type | Required | Description | | ----------- | --------------- | -------- | -------------------------------------------------- | | `namespace` | `string` | No | Filter by namespace (e.g. `"powerhouse"`, `"sky"`) | | `paging` | `PagingOptions` | No | Pagination cursor and limit | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `getDocumentModelModule` Get a specific document model module by document type. ```typescript getDocumentModelModule( documentType: string, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `documentType` | `string` | Yes | e.g. `"powerhouse/todo-list"` | --- ## Write methods All write methods internally create jobs and wait for them to reach `READ_READY` before resolving (except `executeAsync` which returns immediately). ### `create` Create a document from a full `PHDocument` object. ```typescript create( document: PHDocument, parentIdentifier?: string, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------------ | ------------- | -------- | -------------------------------------------------------- | | `document` | `PHDocument` | Yes | Document with optional id, slug, type, and initial state | | `parentIdentifier` | `string` | No | Id or slug of parent document | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `createEmpty` Create an empty document of a given type. ```typescript createEmpty( documentModelType: string, options?: CreateDocumentOptions, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------------- | ----------------------- | -------- | -------------------------------------- | | `documentModelType` | `string` | Yes | e.g. `"powerhouse/todo-list"` | | `options` | `CreateDocumentOptions` | No | Parent identifier and/or model version | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `createDocumentInDrive` Create a document inside a drive as a single batched operation. More efficient than `createEmpty` followed by `addRelationship` because all actions are batched into dependent jobs. ```typescript createDocumentInDrive( driveId: string, document: PHDocument, parentFolder?: string, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | -------------- | ------------- | -------- | -------------------------- | | `driveId` | `string` | Yes | Drive document id or slug | | `document` | `PHDocument` | Yes | The document to create | | `parentFolder` | `string` | No | Folder id within the drive | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `execute` Apply actions to a document and wait for completion. ```typescript execute( documentIdentifier: string, branch: string, actions: Action[], signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | -------------------- | ------------- | -------- | ------------------------------------------ | | `documentIdentifier` | `string` | Yes | Document id or slug | | `branch` | `string` | Yes | Branch to apply actions to (e.g. `"main"`) | | `actions` | `Action[]` | Yes | List of actions to apply | | `signal` | `AbortSignal` | No | Cancel the request | **Returns** the updated document after all actions are applied and read models are updated. **Example:** ```typescript const updated = await reactorClient.execute(docId, "main", [ actions.addTodoItem({ text: "Buy groceries" }), actions.addTodoItem({ text: "Walk the dog" }), ]); ``` --- ### `executeAsync` Submit actions without waiting for completion. Returns a `JobInfo` at `PENDING` status. ```typescript executeAsync( documentIdentifier: string, branch: string, actions: Action[], signal?: AbortSignal, ): Promise ``` Use `waitForJob()` or `getJobStatus()` to track progress. --- ### `rename` Rename a document. ```typescript rename( documentIdentifier: string, name: string, branch?: string, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | -------------------- | ------------- | -------- | -------------------- | | `documentIdentifier` | `string` | Yes | Document id or slug | | `name` | `string` | Yes | New name | | `branch` | `string` | No | Defaults to `"main"` | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `addRelationship` Add a typed relationship from a source document to a target document. To add a child, pass `"child"` as the `relationshipType`. ```typescript addRelationship( sourceIdentifier: string, targetIdentifier: string, relationshipType: string, branch?: string, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------------ | ------------- | -------- | ---------------------------------- | | `sourceIdentifier` | `string` | Yes | Source document id or slug | | `targetIdentifier` | `string` | Yes | Target document id or slug | | `relationshipType` | `string` | Yes | Relationship type (e.g. `"child"`) | | `branch` | `string` | No | Defaults to `"main"` | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `removeRelationship` Remove a typed relationship from a source document to a target document. ```typescript removeRelationship( sourceIdentifier: string, targetIdentifier: string, relationshipType: string, branch?: string, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------------ | ------------- | -------- | ---------------------------------- | | `sourceIdentifier` | `string` | Yes | Source document id or slug | | `targetIdentifier` | `string` | Yes | Target document id or slug | | `relationshipType` | `string` | Yes | Relationship type (e.g. `"child"`) | | `branch` | `string` | No | Defaults to `"main"` | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `moveRelationship` Move a typed relationship from one source document to another, keeping the same target. ```typescript moveRelationship( sourceParentIdentifier: string, targetParentIdentifier: string, targetIdentifier: string, relationshipType: string, branch?: string, signal?: AbortSignal, ): Promise<{ source: PHDocument; target: PHDocument }> ``` **Parameters:** | Name | Type | Required | Description | | ------------------------ | ------------- | -------- | ---------------------------------- | | `sourceParentIdentifier` | `string` | Yes | Current source document id or slug | | `targetParentIdentifier` | `string` | Yes | New source document id or slug | | `targetIdentifier` | `string` | Yes | The target document id or slug | | `relationshipType` | `string` | Yes | Relationship type (e.g. `"child"`) | | `branch` | `string` | No | Defaults to `"main"` | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `deleteDocument` Delete a single document. ```typescript deleteDocument( identifier: string, propagate?: PropagationMode, signal?: AbortSignal, ): Promise ``` **Parameters:** | Name | Type | Required | Description | | ------------ | ----------------- | -------- | --------------------------------- | | `identifier` | `string` | Yes | Document id or slug | | `propagate` | `PropagationMode` | No | `Cascade` to also delete children | | `signal` | `AbortSignal` | No | Cancel the request | --- ### `deleteDocuments` Bulk delete multiple documents. ```typescript deleteDocuments( identifiers: string[], propagate?: PropagationMode, signal?: AbortSignal, ): Promise ``` --- ## Subscriptions ### `subscribe` Subscribe to document change events matching a filter. ```typescript subscribe( search: SearchFilter, callback: (event: DocumentChangeEvent) => void, view?: ViewFilter, ): () => void ``` **Returns** an unsubscribe function. **`DocumentChangeEvent`:** ```typescript type DocumentChangeEvent = { type: DocumentChangeType; documents: PHDocument[]; context?: { parentId?: string; childId?: string; }; }; ``` **`DocumentChangeType`** values: | Value | Description | | --------------- | --------------------------------- | | `Created` | A new document was created | | `Deleted` | A document was deleted | | `Updated` | A document's state changed | | `ParentAdded` | A parent relationship was added | | `ParentRemoved` | A parent relationship was removed | | `ChildAdded` | A child relationship was added | | `ChildRemoved` | A child relationship was removed | **Example:** ```typescript // Watch for all todo-list changes const unsubscribe = reactorClient.subscribe( { type: "powerhouse/todo-list" }, (event) => { if (event.type === DocumentChangeType.Updated) { console.log( "Updated:", event.documents.map((d) => d.header.id), ); } }, ); // Later, stop listening unsubscribe(); ``` --- ## Job tracking ### `getJobStatus` Check the current status of a job. ```typescript getJobStatus( jobId: string, signal?: AbortSignal, ): Promise ``` --- ### `waitForJob` Wait for a job to reach a terminal status (`READ_READY` or `FAILED`). ```typescript waitForJob( jobId: string | JobInfo, signal?: AbortSignal, ): Promise ``` **Example:** ```typescript const job = await reactorClient.executeAsync(docId, "main", actions); console.log(job.status); // "PENDING" const completed = await reactorClient.waitForJob(job); console.log(completed.status); // "READ_READY" or "FAILED" ``` --- ## Advanced reactor usage > Source: https://powerhouse.academy/academy/APIReferences/AdvancedReactorUsage This page covers the low-level `IReactor` interface and the internal components you can access through `ReactorModule`. Most developers should use `IReactorClient` (see [IReactorClient API Reference](/academy/APIReferences/ReactorClient)) โ€” the information here is for advanced scenarios such as: - Building custom tooling or infrastructure on top of the reactor - Working with consistency tokens for read-after-write guarantees - Subscribing directly to internal event bus events - Constructing a reactor with custom storage or executor configurations - Writing integration tests that need access to internals ## IReactor vs IReactorClient `IReactorClient` is a high-level wrapper around `IReactor`. The table below summarizes the key differences: | Aspect | `IReactor` | `IReactorClient` | | ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | **Write return values** | Returns `JobInfo` immediately (fire-and-forget) | Waits for job completion, returns the final document | | **Signing** | Caller passes an `ISigner` explicitly | Client manages signing internally | | **Document lookup** | Separate `get()`, `getBySlug()`, `getByIdOrSlug()` methods | Single `get(identifier)` that accepts either | | **Children/parents** | Returns `string[]` (document IDs only) | Returns `PagedResults` (full documents) | | **Convenience methods** | Basic CRUD | Plus: `createEmpty()`, `createDocumentInDrive()`, `rename()`, `moveRelationship()`, `deleteDocuments()` | | **Subscriptions** | Not available (use the event bus directly) | `subscribe(search, callback, view?)` for real-time document changes | | **Consistency tokens** | Explicit โ€” pass tokens to reads after writes | Handled internally by the client | **When to use `IReactor` directly:** - You need fire-and-forget job submission without waiting for completion - You want explicit control over consistency tokens - You are building infrastructure that manages its own signing - You need access to `executeBatch()` for multi-document atomic operations with dependency ordering ## Building a reactor with ReactorBuilder `ReactorBuilder` uses a fluent API to construct and wire all internal components. ```typescript ReactorBuilder, ConsoleLogger, ChannelScheme, } from "@powerhousedao/reactor"; const reactor = await new ReactorBuilder() .withDocumentModels([todoListModule, invoiceModule]) .withLogger(new ConsoleLogger()) .withExecutorConfig({ maxConcurrency: 4, jobTimeoutMs: 30_000 }) .withWriteCacheConfig({ maxDocuments: 100, ringBufferSize: 10 }) .withMigrationStrategy("auto") .withChannelScheme(ChannelScheme.CONNECT) .build(); ``` `build()` returns an `IReactor`. If you need access to internal components, use `buildModule()` instead โ€” it returns a `ReactorModule` containing the reactor plus all its internals (see [ReactorModule](#reactormodule)). ### Builder methods | Method | Description | | --------------------------------- | ------------------------------------------------------------------------ | | `withDocumentModels(models)` | Register document model modules | | `withUpgradeManifests(manifests)` | Register upgrade manifests for document model versioning | | `withLogger(logger)` | Set the logger (defaults to `ConsoleLogger`) | | `withExecutorConfig(config)` | Configure `maxConcurrency` and `jobTimeoutMs` | | `withWriteCacheConfig(config)` | Configure `maxDocuments` and `ringBufferSize` for the write cache | | `withMigrationStrategy(strategy)` | Set to `"auto"` to run database migrations on build | | `withKysely(kysely)` | Provide a custom Kysely database instance (defaults to in-memory PGlite) | | `withQueue(queue)` | Provide a custom job queue (defaults to `InMemoryQueue`) | | `withEventBus(eventBus)` | Provide a custom event bus | | `withExecutor(executor)` | Provide a custom job executor manager | | `withReadModel(readModel)` | Register an additional read model | | `withSync(syncBuilder)` | Enable synchronization with remote reactors | | `withChannelScheme(scheme)` | Set the sync channel scheme | | `withFeatures(features)` | Set feature flags | | `withSignatureVerifier(verifier)` | Set a signature verification handler | | `withJwtHandler(handler)` | Set a JWT handler for authentication | | `withDocumentModelLoader(loader)` | Set a custom document model loader | | `withSignalHandlers()` | Register OS signal handlers for graceful shutdown | ## IReactor API ### Reading documents ```typescript // By exact ID const doc = await reactor.get(docId); // By slug const doc = await reactor.getBySlug("my-document"); // By either ID or slug (throws if ambiguous) const doc = await reactor.getByIdOrSlug(identifier); // With consistency token for read-after-write const doc = await reactor.get(docId, undefined, token); // Outgoing and incoming relationships (returns string[] of IDs, not full documents) const childIds = await reactor.getOutgoingRelationships(parentId, "child"); const parentIds = await reactor.getIncomingRelationships(childId, "child"); // Search const results = await reactor.find({ type: "powerhouse/todo-list" }); // Operations const ops = await reactor.getOperations(docId); ``` ### Writing documents All write methods return `JobInfo` immediately โ€” they do not wait for the job to complete. ```typescript // Create a document const job = await reactor.create(document, signer); // Execute actions const job = await reactor.execute(docId, "main", actions); // Load pre-existing operations (e.g., from sync) const job = await reactor.load(docId, "main", operations); // Delete const job = await reactor.deleteDocument(docId, signer); // Manage relationships const job = await reactor.addRelationship(parentId, childId1, "child"); const job = await reactor.removeRelationship(parentId, childId1, "child"); ``` ### Batch operations `executeBatch` lets you submit multiple mutation jobs with dependency ordering. Jobs are executed in the order dictated by their `dependsOn` keys. ```typescript const result = await reactor.executeBatch({ jobs: [ { key: "create-drive", documentId: driveId, scope: "global", branch: "main", actions: [createDriveAction], dependsOn: [], }, { key: "add-document", documentId: driveId, scope: "global", branch: "main", actions: [addFileAction], dependsOn: ["create-drive"], // Waits for drive creation }, ], }); // result.jobs["create-drive"] and result.jobs["add-document"] are JobInfo objects ``` ### Job tracking and shutdown ```typescript // Check job status const info = await reactor.getJobStatus(jobId); // info.status is PENDING | RUNNING | WRITE_READY | READ_READY | FAILED // Graceful shutdown const shutdown = reactor.kill(); // shutdown.isShutdown is true immediately await shutdown.completed; // Resolves when all in-flight jobs finish ``` ## Consistency tokens Every write operation returns a `JobInfo` that includes a `ConsistencyToken`. This token captures the exact operation coordinates that the write produced: ```typescript type ConsistencyToken = { version: 1; createdAtUtcIso: string; coordinates: Array<{ documentId: string; scope: string; branch: string; operationIndex: number; }>; }; ``` Pass this token to subsequent reads to guarantee you see the effects of your write: ```typescript const job = await reactor.execute(docId, "main", actions); const token = job.consistencyToken; // This read is guaranteed to include the operations from the write above const doc = await reactor.get(docId, undefined, token); ``` Without a consistency token, reads may return stale data if the read models have not yet indexed the latest operations. `IReactorClient` handles this automatically โ€” it waits for `READ_READY` before returning โ€” but when using `IReactor` directly, consistency tokens give you explicit control. ## ReactorModule `ReactorBuilder.buildModule()` returns a `ReactorModule` that exposes all internal components: ```typescript const module = await new ReactorBuilder() .withDocumentModels([todoListModule]) .buildModule(); const { reactor, eventBus, processorManager, operationStore } = module; ``` ### Available components | Component | Interface | Purpose | | ---------------------- | ----------------------------- | -------------------------------------------------------- | | `reactor` | `IReactor` | The reactor instance | | `eventBus` | `IEventBus` | Internal pub/sub for reactor events | | `queue` | `IQueue` | Job queue with per-document ordering | | `jobTracker` | `IJobTracker` | Tracks job lifecycle (PENDING through READ_READY/FAILED) | | `executorManager` | `IJobExecutorManager` | Manages job executor instances | | `operationStore` | `IOperationStore` | Append-only operation log | | `keyframeStore` | `IKeyframeStore` | Document state snapshots | | `writeCache` | `IWriteCache` | Write-path document cache | | `operationIndex` | `IOperationIndex` | Global ordinal assignment | | `documentView` | `IDocumentView` | Maintains document snapshots for reads | | `documentIndexer` | `IDocumentIndexer` | Tracks document relationships (parent/child graph) | | `readModelCoordinator` | `IReadModelCoordinator` | Dispatches operations to all read models | | `subscriptionManager` | `IReactorSubscriptionManager` | Manages document change subscriptions | | `processorManager` | `IProcessorManager` | Routes operations to user-defined processors | | `database` | `Kysely` | The underlying database connection | | `syncModule` | `SyncModule \| undefined` | Sync infrastructure (if configured) | ## Subscribing to the event bus The `IEventBus` lets you listen to internal reactor events. Subscribers are called sequentially in registration order. ```typescript // Listen for all completed jobs const unsubscribe = module.eventBus.subscribe( ReactorEventTypes.JOB_READ_READY, (type, event) => { console.log("Job completed:", event.jobId); console.log("Operations:", event.operations.length); }, ); // Listen for sync failures module.eventBus.subscribe(SyncEventTypes.SYNC_FAILED, (type, event) => { console.error("Sync failed for job:", event.jobId, event.errors); }); ``` See [Reactor event system](/academy/Architecture/WorkingWithTheReactor#reactor-event-system) for the full list of event types. ## Working with the operation store The `IOperationStore` provides direct access to the append-only operation log. ```typescript const { operationStore } = module; // Get operations since a specific revision const ops = await operationStore.getSince(docId, "global", "main", 5); // Get the latest revision per scope const revisions = await operationStore.getRevisions(docId, "main"); ``` **WARNING:** Writing directly to the operation store bypasses the job queue, reducers, and read models. In almost all cases, use `reactor.execute()` or `reactor.load()` instead. Direct store access is intended for read-only inspection, debugging, and testing. ## Registering custom read models A read model receives operations after each job's write phase completes and builds a derived view of the data. ```typescript class MyCustomReadModel implements IReadModel { async indexOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { // Build your derived view } } } const reactor = await new ReactorBuilder() .withDocumentModels([todoListModule]) .withReadModel(new MyCustomReadModel()) .build(); ``` Read models registered via `withReadModel()` run in the pre-ready phase โ€” they complete before `JOB_READ_READY` fires. This is in contrast to processors (registered via `ProcessorManager`), which run in the post-ready phase. ## Example: integration test setup A common use case for the low-level API is writing integration tests that need full control over the reactor lifecycle: ```typescript ReactorBuilder, ConsoleLogger, ReactorClientBuilder, } from "@powerhousedao/reactor"; async function createTestReactor() { // Build with full access to internals const module = await new ReactorBuilder() .withDocumentModels([todoListModule]) .withLogger(new ConsoleLogger()) .buildModule(); // Optionally wrap with a client for convenience const client = await new ReactorClientBuilder() .withReactorModule(module) .build(); return { module, client }; } // In your test const { module, client } = await createTestReactor(); // Use client for high-level operations const doc = await client.createEmpty("powerhouse/todo-list"); // Use module for low-level inspection const ops = await module.operationStore.getSince( doc.header.id, "global", "main", 0, ); expect(ops.results.length).toBe(1); // Cleanup module.reactor.kill(); ``` --- ## Relational Database > Source: https://powerhouse.academy/academy/APIReferences/RelationalDatabase This page covers the relational database tools available in Powerhouse applications, providing type-safe database operations with real-time updates through PGlite integration.
Introduction for New Developers ### What is the Relational Database API? This API helps you build applications that need to store, query, and analyze data from Powerhouse documents using traditional database tools. If you're familiar with SQL databases like PostgreSQL or MySQL, this will feel familiar while adding powerful real-time capabilities. ### When Should You Use This API? **Perfect for:** - Building dashboards that show live data - Creating reports and analytics - Integrating with existing business tools - Applications needing complex data queries - Systems requiring audit trails of document changes **Not needed for:** - Simple document viewing or editing - Basic CRUD operations on individual documents - Applications with minimal data analysis needs ### Learning Path **New to relational database processors?** Start with our step-by-step tutorial: [Building a Relational Database Processor](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor) - it walks you through creating a complete todo-list processor from scratch. **Ready to implement?** Use this API reference for detailed function signatures, parameters, and advanced patterns. ### Quick Overview: How It Works 1. **Define your data structure** - Describe what information you want to track 2. **Set up processing** - Configure automatic data extraction from document changes 3. **Query your data** - Use familiar database operations with real-time updates 4. **Build your UI** - Connect to any frontend framework or reporting tool ### Key Concepts (Simplified) - **Processors**: Background workers that automatically organize document data - **Live Queries**: Database queries that update automatically when data changes - **Type Safety**: Built-in error prevention that catches mistakes before they happen - **Hooks**: Ready-to-use functions for common data access patterns
## Overview The relational database layer gives you powerful tools to work with data in your Powerhouse applications. You get type-safe queries, real-time updates, and a simple API that feels familiar to React developers. **Key Benefits:** - **Type-safe queries** with full TypeScript support - **Live query capabilities** with real-time updates - **Automatic optimization** to prevent infinite re-renders - **Simple API** that abstracts away complexity - **Smart memorization** for parameters and queries ## Quick Start
Setting up your first relational database query ### Step 1: Define your database schema ```typescript type MyDatabase = { users: { id: number; name: string; email: string; }; posts: { id: number; title: string; content: string; author_id: number; }; }; ``` ### Step 2: Create a typed query hook ```typescript // Create a typed query hook for your processor const useTypedQuery = createProcessorQuery(MyProcessor); ``` ### Step 3: Use it in your component ```typescript // Simple query - no parameters needed export function useUserList(driveId: string) { return useTypedQuery(driveId, (db) => { return db.selectFrom("users").selectAll().compile(); }); } // Query with parameters export function useUserById(driveId: string, userId: number) { return useTypedQuery( driveId, (db, params) => { return db .selectFrom("users") .selectAll() .where("id", "=", params.userId) .compile(); }, { userId }, ); } ``` ### Step 4: Use in your React component ```typescript function UserList({ driveId }: { driveId: string }) { const { isLoading, error, result } = useUserList(driveId); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; if (!result) return
No data
; return (
    {result.rows.map(user => (
  • {user.name} - {user.email}
  • ))}
); } ```
## Core Hooks ### 1. createProcessorQuery()
`createProcessorQuery(ProcessorClass)`: Creates a typed query hook factory for your processor ### Function Name and Signature ```typescript function createProcessorQuery( ProcessorClass: RelationalDbProcessorClass, ): TypedQueryHook; ``` ### Description Creates a typed query hook factory for a specific processor class. This is the main function you'll use to create hooks for querying your relational database. ### Usage Example ```typescript // Create a typed query hook for your processor const useTypedQuery = createProcessorQuery(MyProcessor); // Use it to create specific query hooks export const useUsers = (driveId: string) => { return useTypedQuery(driveId, (db) => { return db.selectFrom("users").selectAll().compile(); }); }; // With parameters export const useUsersByStatus = (driveId: string, status: string) => { return useTypedQuery( driveId, (db, params) => { return db .selectFrom("users") .selectAll() .where("status", "=", params.status) .compile(); }, { status }, ); }; ``` ### Parameters The returned hook accepts: - `driveId`: The ID of the drive - `queryCallback`: Function that receives the database instance and optional parameters - `parameters`: Optional parameters for the query ### Return Value ```typescript { isLoading: boolean; // True while loading or retrying error: Error | null; // Any error that occurred result: LiveQueryResults | null; // Query results with live updates } ``` ### Notes / Caveats - Create one `useTypedQuery` hook per processor - The hook includes automatic retry logic for common errors - Parameters are automatically memoized - Queries are live and will update automatically when data changes
### 2. useRelationalDb()
`useRelationalDb()`: Access the enhanced database instance directly ### Hook Name and Signature ```typescript function useRelationalDb(): IRelationalDbState; ``` ### Description Provides direct access to the enhanced Kysely database instance with live query capabilities. Use this when you need to perform relational database operations outside of the typical query patterns. ### Usage Example ```typescript function DatabaseOperations() { const { db, isLoading, error } = useRelationalDb(); const createUser = async (name: string, email: string) => { if (!db) return; // Direct database operations await db .insertInto('users') .values({ name, email }) .execute(); }; if (isLoading) return
Database initializing...
; if (error) return
Database error: {error.message}
; return ( ); } ``` ### Parameters - `Schema` - TypeScript type defining your database schema ### Return Value ```typescript { db: RelationalDbWithLive | null; // Enhanced Kysely instance with live capabilities isLoading: boolean; // True while database is initializing error: Error | null; // Any initialization error } ``` ### Notes / Caveats - Always check if `db` is not null before using it - The database instance includes both Kysely methods and live query capabilities - Use this for direct database operations like inserts, updates, and deletes - For queries, prefer `createProcessorQuery()` which provides better optimization ### Related Hooks - [`createProcessorQuery`](#1-createprocessorquery) - For optimized queries - [`useRelationalQuery`](#3-userelationalquery) - For manual query control
### 3. useRelationalQuery()
`useRelationalQuery()`: Lower-level hook for manual query control ### Hook Name and Signature ```typescript function useRelationalQuery( ProcessorClass: RelationalDbProcessorClass, driveId: string, queryCallback: ( db: IRelationalQueryBuilder, parameters?: TParams, ) => QueryCallbackReturnType, parameters?: TParams, options?: useRelationalQueryOptions, ): { isLoading: boolean; error: Error | undefined; result: LiveQueryResults | null; }; ``` ### Description Lower-level hook for creating live queries with manual control over the query callback and parameters. Most developers should use `createProcessorQuery()` instead, but this hook is useful for advanced use cases. ### Usage Example ```typescript function UserCount() { const { result, isLoading, error } = useRelationalQuery( MyProcessorClass, driveId, (db) => { return db .selectFrom('users') .select(db.fn.count('id').as('count')) .compile(); } ); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return
User count: {result?.rows[0]?.count ?? 0}
; } ``` ### Parameters - `ProcessorClass` - The processor class with the relational DB schema - `driveId` - The drive ID to scope the query - `queryCallback` - Function that receives the database instance and optional parameters - `parameters` - Optional parameters for the query - `options` - Optional `useRelationalQueryOptions` (e.g. `hashNamespace`) ### Return Value ```typescript { result: LiveQueryResults | null; // Live query results isLoading: boolean; // Combined loading state error: Error | null; // Any error that occurred } ``` ### Notes / Caveats - This hook doesn't include automatic parameter memoization - Use `createProcessorQuery()` for better developer experience and optimization - Useful for cases where you need manual control over the query lifecycle ### Related Hooks - [`createProcessorQuery`](#1-createprocessorquery) - Recommended higher-level API - [`useRelationalDb`](#2-usedelationaldb) - For direct database access
### 4. useRelationalQueryOptions ```typescript export type useRelationalQueryOptions = { hashNamespace?: boolean }; ``` _Description pending โ€” see source._ ## Advanced Patterns ### Working with Dynamic Parameters
How to handle parameters that change over time ### Problem You need to create queries that update automatically when search terms, filters, or other parameters change. ### Solution The `createProcessorQuery` hook automatically handles parameter changes and memoizes them using deep comparison: ```typescript function useSearchResults(driveId: string) { const [searchTerm, setSearchTerm] = useState(""); const [category, setCategory] = useState("all"); // Query automatically updates when searchTerm or category changes const result = useTypedQuery( driveId, (db, params) => { let query = db.selectFrom("products").selectAll(); if (params.searchTerm) { query = query.where("name", "like", `%${params.searchTerm}%`); } if (params.category !== "all") { query = query.where("category", "=", params.category); } return query.compile(); }, { searchTerm, category }, ); return { result, setSearchTerm, setCategory }; } ``` ### Key Points - Parameters are automatically memoized using deep comparison - No need to wrap parameters in `useMemo` - Query re-runs only when parameter values actually change - Works with complex nested objects
### Custom SQL Queries
Using raw SQL instead of Kysely query builder ### Problem You need to write complex SQL queries that are easier to express in raw SQL than using the Kysely query builder. ### Solution You can return raw SQL queries from your callback: ```typescript function useCustomUserStats(driveId: string) { return useTypedQuery(driveId, () => { return { sql: ` SELECT u.name, COUNT(p.id) as post_count, MAX(p.created_at) as last_post_date FROM users u LEFT JOIN posts p ON u.id = p.author_id GROUP BY u.id, u.name ORDER BY post_count DESC `, }; }); } // With parameters function useUserPostsByDateRange( driveId: string, startDate: string, endDate: string, ) { return useTypedQuery( driveId, (db, params) => { return { sql: ` SELECT p.*, u.name as author_name FROM posts p JOIN users u ON p.author_id = u.id WHERE p.created_at BETWEEN $1 AND $2 ORDER BY p.created_at DESC `, parameters: [params.startDate, params.endDate], }; }, { startDate, endDate }, ); } ``` ### Key Points - Return an object with `sql` and optional `parameters` properties - Use parameterized queries ($1, $2, etc.) for dynamic values - You can mix Kysely and raw SQL approaches in the same application
### Complex Joins and Relationships
Working with related data across multiple tables ### Problem You need to fetch related data from multiple tables with complex relationships. ### Solution Use Kysely's join capabilities within your query callbacks: ```typescript function useUsersWithPosts(driveId: string) { return useTypedQuery(driveId, (db) => { return db .selectFrom("users") .leftJoin("posts", "users.id", "posts.author_id") .select([ "users.id", "users.name", "users.email", "posts.title as post_title", "posts.content as post_content", ]) .compile(); }); } // More complex example with multiple joins and aggregations function useUserDashboardData(driveId: string, userId: number) { return useTypedQuery( driveId, (db, params) => { return db .selectFrom("users") .leftJoin("posts", "users.id", "posts.author_id") .leftJoin("comments", "posts.id", "comments.post_id") .select([ "users.id", "users.name", "users.email", db.fn.count("posts.id").as("post_count"), db.fn.count("comments.id").as("comment_count"), ]) .where("users.id", "=", params.userId) .groupBy(["users.id", "users.name", "users.email"]) .compile(); }, { userId }, ); } ``` ### Key Points - Use Kysely's join methods for related data - Leverage aggregation functions for counts and calculations - Type safety is maintained throughout complex queries
## Best Practices ### 1. Schema Definition
How to properly define your database schema types Always define clear TypeScript interfaces for your database schema: ```typescript // โœ… Good - Clear, typed schema type AppDatabase = { users: { id: number; name: string; email: string; created_at: Date; updated_at: Date; }; posts: { id: number; title: string; content: string; author_id: number; published: boolean; created_at: Date; }; }; // โŒ Avoid - Vague or missing types type BadDatabase = { users: any; posts: Record; }; ```
### 2. Hook Organization
How to organize your database hooks Create focused, reusable hooks for different data access patterns: ```typescript // โœ… Good - Focused, reusable hooks export function useUsers(driveId: string) { return useTypedQuery(driveId, (db) => db.selectFrom("users").selectAll().compile(), ); } export function useUserById(driveId: string, id: number) { return useTypedQuery( driveId, (db, params) => db.selectFrom("users").selectAll().where("id", "=", params.id).compile(), { id }, ); } export function useActiveUsers(driveId: string) { return useTypedQuery(driveId, (db) => db.selectFrom("users").selectAll().where("active", "=", true).compile(), ); } // โŒ Avoid - Too generic or complex export function useEverything(driveId: string) { return useTypedQuery(driveId, (db) => db .selectFrom("users") .leftJoin("posts", "users.id", "posts.author_id") .leftJoin("comments", "posts.id", "comments.post_id") .selectAll() // Too much data .compile(), ); } ```
### 3. Error Handling
How to handle loading states and errors Always handle loading and error states in your components: ```typescript function UserList() { const { isLoading, error, result } = useUsers(); // โœ… Good - Handle all states if (isLoading) return ; if (error) return ; if (!result) return ; return (
    {result.rows.map(user => (
  • {user.name}
  • ))}
); } // โŒ Avoid - Missing error handling function BadUserList() { const { result } = useUsers(); return (
    {result?.rows.map(user => (
  • {user.name}
  • ))}
); } ```
### 4. Performance Optimization
Tips for optimal query performance - **Keep queries focused**: Don't select unnecessary columns or join too many tables - **Use parameters wisely**: The automatic memoization handles most cases, but avoid creating new objects unnecessarily - **Consider query frequency**: For data that changes rarely, consider caching strategies ```typescript // โœ… Good - Focused query function useUserNames(driveId: string) { return useTypedQuery(driveId, (db) => db .selectFrom("users") .select(["id", "name"]) // Only what you need .compile(), ); } // โœ… Good - Stable parameters function useUsersByStatus(driveId: string, status: string) { return useTypedQuery( driveId, (db, params) => db .selectFrom("users") .selectAll() .where("status", "=", params.status) .compile(), { status }, // Simple, stable parameter ); } // โŒ Avoid - Unnecessary data function useEverythingAboutUsers(driveId: string) { return useTypedQuery(driveId, (db) => db .selectFrom("users") .leftJoin("posts", "users.id", "posts.author_id") .selectAll() // Too much data .compile(), ); } ```
## Common Issues and Solutions ### Query Not Updating
My query results aren't updating when I expect them to ### Problem Your query results don't update when you expect them to, even though you've changed parameters. ### Solution Check that your parameters are actually changing in content, not just reference: ```typescript // โœ… Good - Parameters change in content const [userId, setUserId] = useState(1); const result = useUserById(driveId, userId); // Updates when userId changes // โŒ Common mistake - Same content, different objects const result = useTypedQuery( driveId, (db, params) => /* query */, { userId: user.id } // New object every render, but same content ); // โœ… Better - Extract stable values const userId = user.id; const result = useTypedQuery( driveId, (db, params) => /* query */, { userId } // Stable parameter ); ``` ### Debugging Tips - Log your parameters to see if they're actually changing - Check the `isLoading` state to see if queries are re-running - Use React DevTools to inspect hook state changes
### Type Errors
Getting TypeScript errors with my queries ### Problem TypeScript is showing errors about query return types or database schema. ### Solution Make sure your callback returns the correct type: ```typescript // โœ… Good - Returns QueryCallbackReturnType const result = useTypedQuery(driveId, (db) => { return db.selectFrom("users").selectAll().compile(); // Has sql property }); // โŒ Error - Missing .compile() const result = useTypedQuery(driveId, (db) => { return db.selectFrom("users").selectAll(); // No sql property }); // โœ… Good - Raw SQL format const result = useTypedQuery(driveId, () => { return { sql: "SELECT * FROM users", parameters: [], }; }); ```
### Performance Issues
My queries are running too frequently or causing lag ### Problem Your queries are running more often than expected, causing performance issues. ### Solution Check for unstable parameters or overly complex queries: ```typescript // โŒ Problem - New object every render function BadComponent({ driveId, user }) { const result = useTypedQuery( driveId, (db, params) => /* query */, { filter: { status: 'active', dept: user.department } // New object each render } ); } // โœ… Solution - Stable parameters function GoodComponent({ driveId, user }) { const filter = useMemo(() => ({ status: 'active', dept: user.department }), [user.department]); const result = useTypedQuery( driveId, (db, params) => /* query */, { filter } ); } // โœ… Even better - Direct values function BetterComponent({ driveId, user }) { const result = useTypedQuery( driveId, (db, params) => /* query */, { status: 'active', dept: user.department } ); } ```
## Further Reading - [Kysely Documentation](https://kysely.dev/) - Learn more about the query builder - [PGlite Documentation](https://pglite.dev/) - Understanding the underlying database - [React Hooks](/academy/APIReferences/ReactHooks) - Other available hooks in Powerhouse - [Component Library](/academy/ComponentLibrary/DocumentEngineering) - Building UI components ## Related Hooks - [`useDocuments`](/academy/APIReferences/ReactHooks#usedocuments) - Working with Powerhouse documents - [`useDrives`](/academy/APIReferences/ReactHooks#usedrives) - Managing document drives - [`useSelectedDocument`](/academy/APIReferences/ReactHooks#useselecteddocument) - Document selection state --- ## PHDocument Migration Guide > Source: https://powerhouse.academy/academy/APIReferences/PHDocumentMigrationGuide **TIP:** This guide covers the **breaking changes** introduced in Powerhouse v4.0.0 related to PHDocument structure changes. If you're upgrading from v3.2.0 or earlier, **this migration is required** and document models must be regenerated. ## Overview Version 4.0.0 introduced a significant refactor of the `PHDocument` structure that consolidates document metadata into a `header` field. This change enables signed and unsigned documents with cryptographic verification capabilities, but requires updating all code that accesses document properties. ## What Changed ### Document Structure Refactor The most significant change is the consolidation of document metadata into a `header` field. Previously, document properties were scattered at the root level of the document object. **Before (v3.2.0 and earlier):** ```javascript const document = { id: "doc-123", created: "2023-01-01T00:00:00.000Z", lastModified: "2023-01-01T12:00:00.000Z", revision: 5, documentType: "powerhouse/todo-list", name: "My Todo List", slug: "my-todo-list", // ... other properties }; ``` **After (v4.0.0):** ```javascript const document = { header: { id: "doc-123", createdAtUtcIso: "2023-01-01T00:00:00.000Z", lastModifiedAtUtcIso: "2023-01-01T12:00:00.000Z", revision: { global: 5, local: 0 }, documentType: "powerhouse/todo-list", name: "My Todo List", slug: "my-todo-list", branch: "main", sig: { nonce: "", publicKey: {} }, meta: {}, }, // ... other properties }; ``` ## Complete Property Migration Map | **Old Property** | **New Property** | **Additional Changes** | | ----------------------- | -------------------------------------- | ------------------------------------------------------------------- | | `document.id` | `document.header.id` | Now an Ed25519 signature for signed documents | | `document.created` | `document.header.createdAtUtcIso` | **Renamed** to include UTC ISO specification | | `document.lastModified` | `document.header.lastModifiedAtUtcIso` | **Renamed** to include UTC ISO specification | | `document.revision` | `document.header.revision` | Now an **object** with scope keys (e.g., `{ global: 5, local: 0 }`) | | `document.documentType` | `document.header.documentType` | No additional changes | | `document.name` | `document.header.name` | No additional changes | | `document.slug` | `document.header.slug` | No additional changes | | `document.branch` | `document.header.branch` | Now explicitly included | | `document.meta` | `document.header.meta` | Now explicitly included | | N/A | `document.header.sig` | **New** - Signature information for document verification | ## Step-by-Step Migration Guide ### Step 1: Update Document Property Access Replace all instances of direct property access with header-based access:
**Common Property Access Patterns** **Document ID Access:** ```javascript // Before const documentId = document.id; // After const documentId = document.header.id; ``` **Document Name Access:** ```javascript // Before const documentName = document.name; // After const documentName = document.header.name; ``` **Document Type Access:** ```javascript // Before const docType = document.documentType; // After const docType = document.header.documentType; ``` **Timestamp Access:** ```javascript // Before const created = document.created; const lastModified = document.lastModified; // After const created = document.header.createdAtUtcIso; const lastModified = document.header.lastModifiedAtUtcIso; ``` **Revision Access:** ```javascript // Before const revision = document.revision; // Was a number // After const globalRevision = document.header.revision.global; // Now an object const localRevision = document.header.revision.local; // Or get all revisions const allRevisions = document.header.revision; // { global: 5, local: 0, ... } ```
### Step 2: Update Component Code **React Components:**
**Example: Document List Component** ```jsx // Before function DocumentList({ documents }) { return (
{documents.map((doc) => (

{doc.name}

Type: {doc.documentType}

Last modified: {new Date(doc.lastModified).toLocaleDateString()}

Revision: {doc.revision}

))}
); } // After function DocumentList({ documents }) { return (
{documents.map((doc) => (

{doc.header.name}

Type: {doc.header.documentType}

Last modified:{" "} {new Date(doc.header.lastModifiedAtUtcIso).toLocaleDateString()}

Global Revision: {doc.header.revision.global}

))}
); } ```
### Step 3: Update Type Definitions If you're using TypeScript, update your type definitions:
**TypeScript Interface Updates** ```typescript // Before interface MyDocument { id: string; name: string; documentType: string; created: string; lastModified: string; revision: number; // ... other properties } // After interface MyDocument { header: { id: string; name: string; documentType: string; createdAtUtcIso: string; lastModifiedAtUtcIso: string; revision: { [scope: string]: number; }; slug: string; branch: string; sig: { nonce: string; publicKey: any; }; meta?: { preferredEditor?: string; }; }; // ... other properties } ```
### Step 4: Database Queries and APIs Compatibility
**GraphQL Query Compatibility** **GraphQL Queries:** ```graphql # Your existing queries continue to work unchanged query GetDocument($id: ID!) { document(id: $id) { id # Still works due to response transformation name # Still works due to response transformation documentType # Still works due to response transformation created # Still works due to response transformation lastModified # Still works due to response transformation revision # Still works due to response transformation } } ``` **TIP:** **GraphQL Backward Compatibility:** The GraphQL API maintains backward compatibility through response transformation. Your existing queries will continue to work without changes. However, when working with the raw document objects in your application code, you'll need to use the new header structure.
## Common Migration Issues and Solutions ### Issue 1: Undefined Property Errors **Problem:** Getting `undefined` when accessing document properties. **Solution:** Update property access to use the header structure: ```javascript // This will be undefined after migration const name = document.name; // Use this instead const name = document.header.name; ``` ### Issue 2: Revision Type Mismatch **Problem:** Code expecting revision to be a number but getting an object. **Solution:** Update revision access to specify the scope: ```javascript // Before - revision was a number if (document.revision > 5) { ... } // After - revision is an object with scope keys if (document.header.revision.global > 5) { ... } ``` ### Issue 3: Date Format Changes **Problem:** Date parsing issues due to property name changes. **Solution:** Update timestamp property names: ```javascript // Before const createdDate = new Date(document.created); const modifiedDate = new Date(document.lastModified); // After const createdDate = new Date(document.header.createdAtUtcIso); const modifiedDate = new Date(document.header.lastModifiedAtUtcIso); ``` ## Testing Your Migration ### Automated Testing Create tests to verify your migration:
**Migration Test Examples** ```javascript // Test document property access describe("Document Migration", () => { it("should access document properties correctly", () => { const mockDocument = { header: { id: "test-id", name: "Test Document", documentType: "powerhouse/test", createdAtUtcIso: "2023-01-01T00:00:00.000Z", lastModifiedAtUtcIso: "2023-01-01T12:00:00.000Z", revision: { global: 5, local: 0 }, // ... other header properties }, // ... other document properties }; // Test property access expect(mockDocument.header.id).toBe("test-id"); expect(mockDocument.header.name).toBe("Test Document"); expect(mockDocument.header.revision.global).toBe(5); }); }); ```
## Related Documentation - [PHDocument Architecture](/academy/Architecture/PowerhouseArchitecture) - [Document Model Creation](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) - [Document Model Versioning](/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning) - For evolving document schemas over time - [React Hooks](/academy/APIReferences/ReactHooks) --- _This migration guide covers the major changes in v4.0.0. For additional technical details, refer to the [RELEASE-NOTES.md](https://github.com/powerhouse-dao/powerhouse/blob/main/RELEASE-NOTES.md) in the main repository._ --- ## Vetra Remote Drive > Source: https://powerhouse.academy/academy/APIReferences/VetraRemoteDrive These commands enable collaborative development using Vetra remote drives. Instead of working with local drives only, you can connect your Powerhouse project to a remote drive that syncs across team members. ## Commands Overview ### `ph init --remote-drive` **Purpose:** Create a new Powerhouse project and connect it to a remote Vetra drive. **When to use:** You're starting a NEW project that will be shared via a remote drive. **How it works:** 1. Validates the remote drive URL and checks that no GitHub URL is configured yet 2. Creates a standard Powerhouse project from the boilerplate 3. Adds Vetra configuration to `powerhouse.config.json`: ```json { "vetra": { "driveId": "abc123", "driveUrl": "https://vetra.example.com/d/abc123" } } ``` **Usage:** ```bash ph init my-project --remote-drive https://vetra.example.com/d/abc123 ``` **After initialization:** - Create a GitHub repository - Commit and push your code - Run `ph vetra` to configure the GitHub URL in the remote drive --- ### `ph checkout --remote-drive` **Purpose:** Clone an existing Powerhouse project that's already connected to a remote drive. **When to use:** You're joining an EXISTING project that someone else initialized. **How it works:** 1. Queries the remote drive to find the configured GitHub URL 2. Clones the repository from GitHub 3. Installs dependencies 4. The project is already configured to use the remote drive **Usage:** ```bash ph checkout --remote-drive https://vetra.example.com/d/abc123 ``` **Requirements:** - The remote drive must have a GitHub URL configured (done during `ph vetra` after init) --- ### `ph vetra` **Purpose:** Start the Vetra development environment with Switchboard and Connect Studio. **When to use:** After initializing or checking out a project, to start development. **How it works:** 1. Reads the remote drive URL from `powerhouse.config.json` (or `--remote-drive` flag) 2. Starts Switchboard connected to the remote drive (syncs automatically) 3. Prompts to configure GitHub URL if not set (first time after init) 4. Starts Connect Studio pointing to the drive(s) **With `--watch` flag:** - Creates a second "Vetra Preview" drive for testing local changes - Dynamically loads your local document models and editors - Main drive stays stable, preview drive for experimentation **Usage:** ```bash # Basic usage (uses config from powerhouse.config.json) ph vetra # With watch mode for development ph vetra --watch # Override remote drive URL ph vetra --remote-drive https://vetra.example.com/d/abc123 # Disable Connect Studio ph vetra --disable-connect ``` **Key options:** - `--watch` - Enable dynamic loading and create preview drive - `--remote-drive ` - Specify remote drive URL - `--switchboard-port ` - Custom Switchboard port (default: 4001) - `--connect-port ` - Custom Connect Studio port (default: 3000) - `--disable-connect` - Skip Connect Studio - `--interactive` - Enable interactive mode for code generation --- ## Workflows ### Starting a New Project (Owner) ```bash # 1. Initialize with remote drive ph init my-project --remote-drive https://vetra.example.com/d/abc123 # 2. Create GitHub repo and push cd my-project git add . git commit -m "Initial commit" git remote add origin https://github.com/user/my-project.git git push -u origin main # 3. Start Vetra and configure GitHub URL ph vetra # (Select option to use detected GitHub URL when prompted) # 4. Start developing with watch mode ph vetra --watch ``` ### Joining an Existing Project (Collaborator) ```bash # 1. Checkout from remote drive ph checkout --remote-drive https://vetra.example.com/d/abc123 # 2. Navigate to project cd project-name # 3. Start Vetra environment with watch mode ph vetra --watch # You're now synced with the remote drive ``` --- ## Key Concepts **Remote Drive vs Local Drive:** - Without remote drive: `ph vetra` creates a local drive on your machine only - With remote drive: `ph vetra` connects to a shared drive that syncs across team members **When to use each command:** - Use `ph init --remote-drive` when starting a NEW project (no GitHub URL configured in drive yet) - Use `ph checkout --remote-drive` when joining an EXISTING project (GitHub URL already configured) - Use `ph vetra --watch` to start development after either init or checkout **Preview Drive (`--watch` mode):** - Main "Vetra" drive: syncs with remote, contains stable package configuration - "Vetra Preview" drive: created locally with `--watch`, for testing local document models - Without `--watch`: safer, prevents untested code from affecting Connect - With `--watch`: enables rapid development and testing --- ## Subgraph Migration Guide (v6 Reactor) > Source: https://powerhouse.academy/academy/APIReferences/SubgraphMigrationGuide **TIP:** This guide covers the **breaking changes** to the GraphQL subgraph API introduced in the v6 Reactor. If you were querying the old `/graphql/document-drive` or `/graphql/system` endpoints, or building custom subgraphs, **this migration is required**. ## Overview The v6 Reactor replaced the legacy hardcoded subgraphs (`document-drive`, `system`) with a new architecture: - **Reactor subgraph** (`/graphql/r`) โ€” manages drives, documents, sync, and subscriptions - **Document-model subgraphs** (`/graphql/`) โ€” auto-generated per document model, with namespaced queries and mutations - **Custom subgraphs** โ€” user-defined subgraphs now extend `BaseSubgraph` with `reactorClient` ## Endpoint changes | Legacy endpoint | v6 endpoint | Notes | | ------------------------- | ----------------------- | --------------------------------------- | | `/graphql/document-drive` | `/graphql/r` | Drive and document management | | `/graphql/system` | `/graphql/r` | Sync operations moved to reactor | | `/graphql/` | `/graphql/` | Custom subgraphs still user-defined | | N/A | `/graphql/` | New: auto-generated per document model | | `/graphql` | `/graphql` | Supergraph still combines all subgraphs | ## Querying and mutating documents In v6, every operation can be done through the **reactor subgraph** (`/graphql/r`) โ€” which is generic and works with any document type โ€” or through a **document-model subgraph** (`/graphql/`) โ€” which is namespaced and type-specific. Both are shown below for each operation. Document-model subgraphs are auto-generated for each registered document model and provide: - `document(identifier)` โ€” get a single document - `documents(paging)` โ€” list all documents of this type - `findDocuments(search, view, paging)` โ€” search within this type - `documentOutgoingRelationships(sourceIdentifier, relationshipType)` โ€” filtered to this type - `documentIncomingRelationships(targetIdentifier, relationshipType)` - `createDocument(name, parentIdentifier)` mutation - Per-operation mutations (e.g. `addTodoItem(docId, input)`) - Async variants of each mutation (e.g. `addTodoItemAsync(docId, input)`) ### Getting a drive and its contents **Legacy:** ```graphql # Legacy โ€” /graphql/document-drive query { drive { id name nodes { ... on FileNode { id name documentType } ... on FolderNode { id name } } } } ``` **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r query { document(identifier: "my-drive-slug") { document { id name documentType state } childIds } } ``` **v6 (document-model subgraph):** ```graphql # v6 โ€” /graphql/document-drive (or via supergraph) query { DocumentDrive { document(identifier: "my-drive-slug") { document { id name state { global { nodes { ... } } } } } } } ``` ### Listing children of a drive **Legacy:** Children were returned inline via the `drive.nodes` field (see above). **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r query { documentOutgoingRelationships( sourceIdentifier: "my-drive-slug" relationshipType: "child" ) { items { id name documentType state } totalCount hasNextPage } } ``` **v6 (document-model subgraph):** To get only children of a specific type (e.g. todo lists): ```graphql # v6 โ€” /graphql/to-do-list (or via supergraph) query { ToDoList { documentOutgoingRelationships( sourceIdentifier: "my-drive-slug" relationshipType: "child" ) { items { id name state { global { items { id text checked } } } } totalCount } } } ``` ### Finding documents by type **Legacy:** Not directly available โ€” required iterating `drive.nodes` and filtering by `documentType`. **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r query { findDocuments(search: { type: "powerhouse/todo-list" }) { items { id name state } totalCount } } ``` **v6 (document-model subgraph):** The type filter is built in: ```graphql # v6 โ€” /graphql/to-do-list (or via supergraph) query { ToDoList { documents { items { id name state { global { items { id text checked } } } } totalCount } } } ``` ### Getting a single document **Legacy:** ```graphql # Legacy โ€” /graphql/document-drive query { document(id: "abc123") { id name # ... limited to drive-level fields } } ``` **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r query { document(identifier: "abc123") { document { id name documentType state } childIds } } ``` **v6 (document-model subgraph):** ```graphql # v6 โ€” /graphql/to-do-list (or via supergraph) query { ToDoList { document(identifier: "abc123") { document { id name state { global { items { id text checked } } } } } } } ``` ### Creating a drive **Legacy:** ```graphql # Legacy โ€” /graphql/document-drive mutation { addDrive(name: "tutorial") { id name } } ``` **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r mutation { createDocument( document: { documentType: "powerhouse/document-drive", name: "tutorial" } ) { id name } } ``` **v6 (document-model subgraph):** ```graphql # v6 โ€” /graphql/document-drive (or via supergraph) mutation { DocumentDrive { createDocument(name: "tutorial") { id name } } } ``` ### Creating a document In the legacy system, documents were created indirectly through drive operations or via Connect's internal APIs. In v6, documents are created directly. **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r mutation { createDocument( document: { documentType: "powerhouse/todo-list", name: "My List" } parentIdentifier: "my-drive-slug" ) { id name } } ``` **v6 (document-model subgraph):** ```graphql # v6 โ€” /graphql/to-do-list (or via supergraph) mutation { ToDoList { createDocument(name: "My List", parentIdentifier: "my-drive-slug") { id name } } } ``` ### Applying operations to a document In the legacy system, document operations were applied through strand-based push/pull via the `system` subgraph. In v6, operations are applied directly. **v6 (reactor subgraph):** ```graphql # v6 โ€” /graphql/r mutation { mutateDocument( documentIdentifier: "abc123" actions: [ { type: "ADD_TODO_ITEM" input: { text: "Buy milk" } scope: "global" id: "op-1" timestampUtcMs: "1711900000000" } ] ) { id name state } } ``` **v6 (document-model subgraph):** ```graphql # v6 โ€” /graphql/to-do-list (or via supergraph) mutation { ToDoList { addTodoItem(docId: "abc123", input: { text: "Buy milk" }) { id state { global { items { id text checked } } } } } } ``` ## Sync API changes ### Legacy: system subgraph (strand-based sync) The old `system` subgraph provided strand-based synchronization via pull-responder listeners: ```graphql # Legacy โ€” /graphql/system mutation { registerPullResponderListener(filter: { documentType: ["powerhouse/todo-list"] branch: ["main"] }) { # returned a Listener object } } query { system { sync { strands(listenerId: "listener-123") { driveId documentId scope branch operations { type input index hash timestamp } } } } } ``` ### v6: Channel-based sync The v6 reactor replaces the strand/listener model with channel-based sync and real-time subscriptions: ```graphql # v6 โ€” /graphql/r mutation { touchChannel( input: { id: "channel-1" name: "my-sync" collectionId: "drive-id" filter: { documentId: ["*"], scope: ["global"], branch: "main" } sinceTimestampUtcMs: "0" } ) { success ackOrdinal } } query { pollSyncEnvelopes(channelId: "channel-1", outboxAck: 0, outboxLatest: 100) { envelopes { type operations { operation { index hash action { type input scope } } context { documentId documentType scope branch ordinal } } } ackOrdinal } } ``` Real-time updates are available via GraphQL subscriptions: ```graphql subscription { documentChanges(search: { type: "powerhouse/todo-list" }) { type # CREATED, UPDATED, DELETED, etc. documents { id name state } } } ``` ## Query mapping reference | Legacy query/mutation | v6 equivalent | Subgraph | | ------------------------------- | ------------------------------------------------------------------------------ | ---------------------- | | `drive` | `document(identifier: driveSlug)` | reactor (`/graphql/r`) | | `drives` | `findDocuments(search: { type: "powerhouse/document-drive" })` | reactor | | `document(id)` | `document(identifier)` | reactor | | `documents` | `findDocuments()` | reactor | | `addDrive(name)` | `createDocument(document: { documentType: "powerhouse/document-drive", ... })` | reactor | | `system { sync { strands } }` | `pollSyncEnvelopes(channelId, ...)` | reactor | | `registerPullResponderListener` | `touchChannel(input: {...})` | reactor | | `pushUpdates` | `pushSyncEnvelopes(envelopes: [...])` | reactor | | N/A (new) | ` { document(...) }` | document-model | | N/A (new) | ` { createDocument(...) }` | document-model | | N/A (new) | ` { (docId, input) }` | document-model | ## Migrating custom subgraphs ### Legacy pattern Custom subgraphs exported a `resolvers` object and a `typeDefs` string, using `ctx.driveServer` for data access: ```typescript // Legacy custom subgraph export const typeDefs = ` type Query { myCustomQuery(driveId: String!): [String!]! } `; export const resolvers: GraphQLResolverMap = { Query: { myCustomQuery: async (_parent, args, ctx: Context) => { const drive = await ctx.driveServer.getDrive(args.driveId); // ... process drive data return results; }, }, }; ``` ### v6 pattern Custom subgraphs now use `getResolvers(subgraph: BaseSubgraph)` and access data through `subgraph.reactorClient`: ```typescript // v6 custom subgraph โ€” subgraphs/my-custom/resolvers.ts export const getResolvers = (subgraph: BaseSubgraph) => { const reactorClient = subgraph.reactorClient; return { Query: { myCustomQuery: async (_parent: unknown, args: { driveId: string }) => { const children = await reactorClient.getOutgoingRelationships( args.driveId, "child", ); // ... process documents return results; }, }, }; }; ``` ```typescript // v6 custom subgraph โ€” subgraphs/my-custom/schema.ts export const schema: DocumentNode = gql` type Query { myCustomQuery(driveId: String!): [String!]! } `; ``` Generate the scaffolding with: ```bash ph generate --subgraph my-custom ``` ### Key differences | Aspect | Legacy | v6 | | -------------------- | ------------------------------- | ----------------------------------------------- | | Data access | `ctx.driveServer` | `subgraph.reactorClient` | | Schema format | Template literal string | `gql` tagged template (`DocumentNode`) | | Resolver export | `export const resolvers` | `export const getResolvers = (subgraph) => ...` | | Relational DB access | Not available | `subgraph.relationalDb` | | File structure | `resolvers.ts` + `type-defs.ts` | `resolvers.ts` + `schema.ts` + `index.ts` | | Registration | Manual | Automatic via `ph generate` | ## Migration checklist - [ ] Update all GraphQL client queries that target `/graphql/document-drive` to use `/graphql/r` or the appropriate document-model subgraph - [ ] Replace `drive` queries with `document(identifier)` or `findDocuments` - [ ] Replace strand-based sync (`registerPullResponderListener`, `system.sync.strands`) with `touchChannel` + `pollSyncEnvelopes` - [ ] For real-time updates, use `documentChanges` subscription instead of polling strands - [ ] Migrate custom subgraphs to `getResolvers(subgraph: BaseSubgraph)` pattern - [ ] Replace `ctx.driveServer` calls with `subgraph.reactorClient` methods - [ ] Update schema files from plain strings to `gql` tagged templates - [ ] Regenerate custom subgraph scaffolding with `ph generate --subgraph ` --- ## Processor Migration Guide (v6 Reactor) > Source: https://powerhouse.academy/academy/APIReferences/ProcessorMigrationGuide **TIP:** This guide covers the **breaking changes** to the processor interface introduced in the v6 Reactor. If you have existing processors built on the legacy strand-based API, **this migration is required**. ## Overview The v6 Reactor replaced the strand-based processor model with a flat operation-based model: - **Old**: Processors received `InternalTransmitterUpdate[]` strands via `onStrands()`, grouped by document - **New**: Processors receive a flat `OperationWithContext[]` list via `onOperations()`, with per-operation context This change simplifies the processor interface, improves cross-document ordering via the `ordinal` field, and unifies all processor types under a single `IProcessor` interface. ## Import path changes | Legacy import | v6 import | | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | `import { InternalTransmitterUpdate, IProcessor } from "document-drive"` | `import type { IProcessor, OperationWithContext } from "@powerhousedao/reactor-browser"` | | `import type { ReactorContext } from "document-drive"` | Removed โ€” no longer needed | | `import type { OperationWithContext } from "@powerhousedao/reactor"` | Same (server-side alternative to reactor-browser) | **INFO:** `@powerhousedao/reactor-browser` re-exports all reactor types for convenience in browser environments (editors, drive-apps). If you are working outside the browser (Node.js, CLI tools, server-side code), import directly from `@powerhousedao/reactor` or `@powerhousedao/shared`. ## Interface changes ### Legacy: IProcessor with onStrands ```typescript // Legacy processor interface export class MyProcessor implements IProcessor { // Received grouped strands โ€” one per (driveId, documentId, scope, branch) async onStrands(strands: InternalTransmitterUpdate[]): Promise { for (const strand of strands) { const { driveId, documentId, scope, branch } = strand; for (const operation of strand.operations) { // operation.type, operation.input, operation.index, ... } } } // Also required but often unused onOperations(operations: any[]): Promise { return Promise.resolve(); } onDisconnect(): Promise { return Promise.resolve(); } } ``` ### v6: IProcessor with onOperations ```typescript // v6 processor interface export class MyProcessor implements IProcessor { // Receives a flat list of operations with full context async onOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { // context: documentId, documentType, scope, branch, ordinal, resultingState // operation: action (type, input), index, timestampUtcMs, hash } } async onDisconnect(): Promise { // cleanup } } ``` ### Key differences | Aspect | Legacy | v6 | | ----------------------- | ------------------------------------------- | ------------------------------------------------- | | Entry method | `onStrands(strands)` | `onOperations(operations)` | | Data shape | Grouped by document (`strand.operations[]`) | Flat list (`OperationWithContext[]`) | | Document context | `strand.driveId`, `strand.documentId` | `context.documentId`, `context.documentType` | | Operation access | `operation.type`, `operation.input` | `operation.action.type`, `operation.action.input` | | Cross-document ordering | Not available | `context.ordinal` (global monotonic counter) | | Document state | Not provided | `context.resultingState` (JSON string, optional) | | Drive ID | `strand.driveId` | Available via factory's `driveHeader.id` | ## Factory changes ### Legacy factory ```typescript // Legacy processor factory export const myProcessorFactory = (analyticsStore: IAnalyticsStore) => (driveId: string): ProcessorRecord[] => { return [ { processor: new MyProcessor(analyticsStore), filter: { branch: ["main"], documentId: ["*"], scope: ["*"], documentType: ["*"], }, }, ]; }; ``` ### v6 factory ```typescript // v6 processor factory ProcessorRecord, IProcessorHostModule, } from "@powerhousedao/reactor-browser"; export const myProcessorFactory = (module: IProcessorHostModule) => ( driveHeader: PHDocumentHeader, processorApp?: ProcessorApp, ): ProcessorRecord[] => { return [ { processor: new MyProcessor(module.analyticsStore), filter: { branch: ["main"], documentId: ["*"], scope: ["*"], documentType: ["*"], }, }, ]; }; ``` ### Factory differences | Aspect | Legacy | v6 | | ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------- | | Outer parameter | Direct dependencies (e.g. `analyticsStore`) | `IProcessorHostModule` (bundles `analyticsStore`, `relationalDb`, `processorApp`, `config`) | | Inner parameter | `driveId: string` | `driveHeader: PHDocumentHeader` (full header with `id`, `name`, `documentType`, etc.) | | Optional parameter | None | `processorApp?: ProcessorApp` | ## Migrating an analytics processor ### Legacy analytics processor ```typescript // Legacy AnalyticsSeriesInput, IAnalyticsStore, } from "@powerhousedao/analytics-engine-core"; export class MyAnalyticsProcessor implements IProcessor { private readonly inputs: AnalyticsSeriesInput[] = []; constructor(private readonly analyticsStore: IAnalyticsStore) {} onOperations(): Promise { return Promise.resolve(); } onDisconnect(): Promise { return Promise.resolve(); } async onStrands(strands: InternalTransmitterUpdate[]): Promise { for (const strand of strands) { if (strand.operations.length === 0) continue; const source = AnalyticsPath.fromString( `/MyAnalytics/${strand.driveId}/${strand.documentId}/${strand.branch}/${strand.scope}`, ); if (strand.operations[0].index === 0) { await this.clearSource(source); } for (const operation of strand.operations) { this.inputs.push({ source, metric: "MyMetric", start: DateTime.fromISO(operation.timestamp), value: 1, dimensions: { /* ... */ }, }); } } if (this.inputs.length > 0) { await this.analyticsStore.addSeriesValues(this.inputs); this.inputs.length = 0; } } private async clearSource(source: AnalyticsPath) { try { await this.analyticsStore.clearSeriesBySource(source, true); } catch (e) { console.error(e); } } } ``` ### v6 analytics processor ```typescript // v6 AnalyticsSeriesInput, IAnalyticsStore, } from "@powerhousedao/analytics-engine-core"; OperationWithContext, IProcessor, } from "@powerhousedao/reactor-browser"; export class MyAnalyticsProcessor implements IProcessor { constructor(private readonly analyticsStore: IAnalyticsStore) {} async onOperations(operations: OperationWithContext[]): Promise { if (operations.length === 0) return; const CHUNK_SIZE = 50; const buffer: AnalyticsSeriesInput[] = []; for (const { operation, context } of operations) { const { documentId, branch, scope } = context; const source = AnalyticsPath.fromString( `ph/my-analytics/${documentId}/${branch}/${scope}`, ); buffer.push({ source, metric: "MyMetric", start: DateTime.fromISO(operation.action.timestampUtcMs), value: 1, dimensions: { /* ... */ }, }); while (buffer.length >= CHUNK_SIZE) { const batch = buffer.splice(0, CHUNK_SIZE); await this.analyticsStore.addSeriesValues(batch); } } if (buffer.length > 0) { await this.analyticsStore.addSeriesValues(buffer); } } async onDisconnect(): Promise {} } ``` ### What changed - `onStrands()` is removed entirely โ€” all logic moves to `onOperations()` - **`clearSource` is no longer needed.** The v6 processor manager guarantees each operation is delivered exactly once โ€” there is no replay. You can remove all `clearSource` / `clearSeriesBySource` logic and the `index === 0` guard. - Strand fields like `strand.driveId`, `strand.documentId` become `context.documentId`, `context.documentType`, etc. - `operation.type` becomes `operation.action.type` - `operation.input` becomes `operation.action.input` - `operation.timestamp` becomes `operation.action.timestampUtcMs` - Chunked batch insert is now the recommended pattern (see example above) ## Migrating a relational database processor ### Legacy relational processor Legacy relational processors (previously called "operational processors") were plain `IProcessor` implementations that managed their own database connection: ```typescript // Legacy export class MyRelationalProcessor implements IProcessor { constructor(private db: any) {} async onStrands(strands: InternalTransmitterUpdate[]): Promise { for (const strand of strands) { for (const operation of strand.operations) { await this.db .insertInto("my_table") .values({ doc_id: strand.documentId, action: operation.type, }) .execute(); } } } onOperations(): Promise { return Promise.resolve(); } onDisconnect(): Promise { return Promise.resolve(); } } ``` ### v6 relational database processor The v6 Reactor provides a `RelationalDbProcessor` base class with built-in database lifecycle management: ```typescript // v6 export class MyRelationalProcessor extends RelationalDbProcessor { // Unique namespace per drive โ€” prevents data collisions static override getNamespace(driveId: string): string { return super.getNamespace(driveId); } // Run migrations on startup override async initAndUpgrade(): Promise { await up(this.relationalDb); } override async onOperations( operations: OperationWithContext[], ): Promise { if (operations.length === 0) return; for (const { operation, context } of operations) { await this.relationalDb .insertInto("my_table") .values({ doc_id: context.documentId, action: operation.action.type, }) .onConflict((oc) => oc.column("doc_id").doNothing()) .execute(); } } async onDisconnect(): Promise {} } ``` ### v6 relational factory ```typescript ProcessorRecord, IProcessorHostModule, ProcessorFilter, } from "@powerhousedao/reactor-browser"; export const myRelationalProcessorFactory = (module: IProcessorHostModule) => async (driveHeader: PHDocumentHeader): Promise => { const namespace = MyRelationalProcessor.getNamespace(driveHeader.id); const store = await module.relationalDb.createNamespace( namespace, ); const filter: ProcessorFilter = { branch: ["main"], documentId: ["*"], documentType: ["powerhouse/my-doc-type"], scope: ["global"], }; const processor = new MyRelationalProcessor(namespace, filter, store); return [{ processor, filter }]; }; ``` Generate the scaffolding with: ```bash ph generate --processor my-processor --processor-type relationalDb --document-types powerhouse/my-doc-type ``` ### Key additions in RelationalDbProcessor | Feature | Description | | ------------------------------ | ------------------------------------------------------------------------ | | `RelationalDbProcessor` | Type-safe base class with `this.relationalDb` | | `getNamespace(driveId)` | Static method for per-drive database isolation | | `initAndUpgrade()` | Lifecycle hook for running migrations on startup | | `query(driveId, relationalDb)` | Static method for querying from subgraphs | | Type generation | `ph generate --migration-file migrations.ts` generates `schema.ts` types | ## Migrating a plain processor ### Legacy ```typescript export class LoggerProcessor implements IProcessor { async onStrands(strands: InternalTransmitterUpdate[]): Promise { for (const strand of strands) { for (const op of strand.operations) { console.log(`[${strand.driveId}] ${strand.documentId}: ${op.type}`); } } } onOperations(): Promise { return Promise.resolve(); } onDisconnect(): Promise { return Promise.resolve(); } } ``` ### v6 ```typescript export class LoggerProcessor implements IProcessor { constructor(private driveId: string) {} async onOperations(operations: OperationWithContext[]): Promise { for (const { operation, context } of operations) { console.log( `[${this.driveId}] ${context.documentId}: ${operation.action.type}`, ); } } async onDisconnect(): Promise {} } ``` ## OperationWithContext reference Each item in the `onOperations` list destructures as `{ operation, context }`: **`context`** โ€” where the operation happened: | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------------------- | | `documentId` | `string` | The document that was modified | | `documentType` | `string` | e.g. `"powerhouse/todo-list"` | | `scope` | `string` | The scope (e.g. `"global"`, `"local"`) | | `branch` | `string` | The branch (e.g. `"main"`) | | `ordinal` | `number` | Global monotonically increasing ordinal for cross-document ordering | | `resultingState` | `string?` | JSON string of the document state after the operation | **`operation`** โ€” what happened: | Field | Type | Description | | ---------------- | -------- | ---------------------------------------------------------------------------------- | | `action` | `Action` | Contains `type` (e.g. `"ADD_TODO_ITEM"`), `input`, `timestampUtcMs`, `id`, `scope` | | `index` | `number` | Position in the operation history | | `timestampUtcMs` | `string` | When the operation was created | | `hash` | `string` | Hash of the resulting document state | ## Migration checklist - [ ] Replace all `onStrands(strands: InternalTransmitterUpdate[])` with `onOperations(operations: OperationWithContext[])` - [ ] Remove the `onStrands` method entirely - [ ] Update imports from `document-drive` to `@powerhousedao/reactor-browser` (or `@powerhousedao/reactor` for server-side) - [ ] Replace `strand.driveId` / `strand.documentId` with `context.documentId` / `context.documentType` - [ ] Replace `operation.type` with `operation.action.type` - [ ] Replace `operation.input` with `operation.action.input` - [ ] Replace `operation.timestamp` with `operation.action.timestampUtcMs` - [ ] Update factory signature to accept `IProcessorHostModule` and `PHDocumentHeader` - [ ] For relational processors: extend `RelationalDbProcessor` and implement `initAndUpgrade()` - [ ] Regenerate processor scaffolding with `ph generate --processor ` --- ## Renown SDK > Source: https://powerhouse.academy/academy/APIReferences/renown-sdk/Overview A comprehensive SDK for integrating Renown authentication and user profile management into your React applications. ## Features - ๐Ÿ” **Authentication** - Complete authentication flow with session management - ๐Ÿ‘ค **User Profiles** - Fetch and manage user profile data - โš›๏ธ **React Integration** - Provider and hooks for seamless React integration - ๐ŸŽจ **UI Component** - Ready-to-use RenownAuthButton component - ๐Ÿ”„ **Session Persistence** - Automatic session restoration across page reloads - ๐ŸŒ **Renown Portal** - Easy integration with Renown authentication portal - ๐Ÿ“ฆ **Type-Safe** - Full TypeScript support - ๐ŸŽฏ **Headless** - Customizable UI with optional render props ## Installation ```bash npm install @renown/sdk # or yarn add @renown/sdk # or pnpm add @renown/sdk ``` ## Quick Start ### 1. Wrap Your App with RenownUserProvider The RenownUserProvider automatically initializes the SDK - no manual setup needed! ```typescript // app/layout.tsx or app.tsx export default function RootLayout({ children }) { return ( {children} ) } ``` Optional: Customize with loading/error components: ```typescript

Initializing...

} errorComponent={(error, retry) => (

Failed to initialize

{error.message}

)} > {children} ``` ### 2. Use Authentication in Components ```typescript // components/Header.tsx export function Header() { return (

My App

) } ``` Or use the `useUser` hook for custom implementations: ```typescript // components/CustomAuth.tsx export function CustomAuth() { const { user, loginStatus, openRenown, logout } = useUser() if (user) { return (

Welcome, {user.name || user.did}

) } return } ``` ## Documentation Structure - **[Authentication Guide](../docs/docs/01-Authentication.md)** - Comprehensive guide to implementing authentication - **[API Reference](../docs/docs/02-APIReference.md)** - Complete API documentation ## Key Concepts ### RenownUserProvider The `` component is the central authentication provider that manages auth state across your application. It must wrap your application to provide authentication context. ### useUser Hook The `useUser()` hook provides access to authentication state and methods throughout your application. It can only be used within a ``. The hook returns `connectCrypto` and `renown` instances for advanced use cases. ### Session Management The SDK automatically manages user sessions using sessionStorage, allowing users to stay logged in across page reloads within the same browser session. ### Profile Data User profile data is automatically fetched from the Renown API after successful authentication, enriching the user object with display name, avatar, and other profile information. ### UI Component The SDK provides a ready-to-use component: - **RenownAuthButton** - Smart component that adapts to auth state (shows login button or user info) This component is optional - you can build your own UI using the `useUser` hook. ## Examples ### Next.js App Router ```typescript // app/layout.tsx - Minimal setup export default function RootLayout({ children }) { return ( {children} ) } // components/Navbar.tsx - Using RenownAuthButton component 'use client' export function Navbar() { return ( ) } // app/profile/page.tsx - Using useUser hook 'use client' export default function ProfilePage() { const { user, openRenown, logout } = useUser() if (!user) { return (

Login Required

) } return (

Profile

DID: {user.did}

Name: {user.name}

{user.avatar && Avatar}
) } ``` ### React SPA ```typescript // main.tsx ReactDOM.createRoot(document.getElementById('root')!).render( ) // App.tsx - Using RenownAuthButton function App() { return (

My App

) } // Or custom with useUser hook function CustomApp() { const { user, openRenown } = useUser() return (
{user ? (

Welcome {user.name}

) : ( )}
) } ``` ## Configuration ### RenownUserProvider Props Customize the Renown SDK initialization: ```typescript } // Custom loading screen errorComponent={(error, retry) => } // Custom error screen > ``` All props are optional - RenownUserProvider uses sensible defaults. ### Environment Variables You can use environment variables for configuration: ```typescript ``` ```bash # .env NEXT_PUBLIC_RENOWN_URL=https://www.renown.id ``` ## Troubleshooting ### Context Error **Error:** `useUser must be used within a RenownUserProvider` **Solution:** Ensure your component is wrapped by ``: ```typescript {/* โœ… Can use useUser */} ``` ### Custom Renown URL If you need to use a different Renown instance: ```typescript ``` ## Resources - [GitHub Repository](https://github.com/powerhouse-inc/powerhouse) - [NPM Package](https://www.npmjs.com/package/@renown/sdk) - [Renown Portal](https://www.renown.id) ## License AGPL-3.0-only --- ## Authentication Guide > Source: https://powerhouse.academy/academy/APIReferences/renown-sdk/Authentication Comprehensive guide to implementing authentication with the Renown SDK. ## Table of Contents - [Overview](#overview) - [Authentication Flow](#authentication-flow) - [Setup](#setup) - [Implementation](#implementation) - [Advanced Patterns](#advanced-patterns) - [Security Considerations](#security-considerations) ## Overview The Renown SDK provides a complete authentication system that: 1. **Auto-initializes** the Renown SDK and ConnectCrypto (zero configuration!) 2. **Manages** user sessions across page reloads 3. **Handles** authentication redirects from Renown portal 4. **Fetches** user profile data automatically 5. **Provides** React hooks, components, and context for easy integration ## Authentication Flow ### 1. Initial Load ``` User visits app โ†“ RenownUserProvider initializes โ†“ Check sessionStorage for existing session โ†“ โ”œโ”€ Session found โ†’ Restore user โ†’ Authorized โ””โ”€ No session โ†’ Check URL params โ†’ Initialize SDK โ†’ Not Authorized ``` ### 2. Login Flow ``` User clicks "Login" โ†“ openRenown() called โ†“ Redirect to Renown Portal โ†“ User authenticates โ†“ Redirect back to app with DID โ†“ handleRenownReturn() processes โ†“ login() called โ†“ Fetch user profile โ†“ Store in sessionStorage โ†“ Update auth state โ†’ Authorized ``` ### 3. Session Restoration ``` User refreshes page โ†“ RenownUserProvider checks sessionStorage โ†“ Valid session found โ†“ Fetch latest profile data โ†“ Restore auth state โ†’ Authorized ``` ## Setup ### Step 1: Wrap Your App with RenownUserProvider The RenownUserProvider automatically initializes the Renown SDK - no manual setup required! ```typescript // app/layout.tsx (Next.js App Router) export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` That's it! The SDK is now initialized and ready to use. ### Optional: Customize Configuration You can customize the Renown URL, network, and chain: ```typescript {children} ``` ### Optional: Add Loading/Error Screens Provide custom UI for initialization states: ```typescript

Initializing authentication...

} errorComponent={(error, retry) => (

Authentication Failed

{error.message}

)} > {children} ``` ## Implementation ### Using the RenownAuthButton Component The simplest way to add authentication is to use the built-in `RenownAuthButton` component: ```typescript 'use client' export function Header() { return (

My App

) } ``` ### Custom Login Component with useUser Hook For full control, build your own component using the `useUser` hook: ```typescript 'use client' export function CustomRenownAuthButton() { const { user, loginStatus, isLoading, openRenown, logout } = useUser() if (isLoading) { return ( ) } if (loginStatus === 'authorized' && user) { return (
{user.avatar && ( {user.name )} {user.name || user.did.slice(0, 15)}...
) } return ( ) } ``` ### Protected Route Component ```typescript 'use client' export function ProtectedRoute({ children }: { children: React.ReactNode }) { const { user, loginStatus, isLoading, openRenown } = useUser() const router = useRouter() useEffect(() => { if (!isLoading && loginStatus !== 'authorized') { // Redirect to login or show login prompt router.push('/login') } }, [isLoading, loginStatus, router]) if (isLoading) { return
Loading...
} if (loginStatus !== 'authorized' || !user) { return (

Authentication Required

Please log in to access this page

) } return <>{children} } ``` ### User Profile Display ```typescript 'use client' export function UserProfile() { const { user, logout } = useUser() if (!user) return null return (
{user.avatar && ( {user.name )}

{user.name || 'Anonymous User'}

DID
{user.did}
{user.ethAddress && ( <>
Ethereum Address
{user.ethAddress}
)} {user.email && ( <>
Email
{user.email}
)}
) } ``` ## Advanced Patterns ### Conditional Navigation Based on Auth ```typescript 'use client' export function Navigation() { const { user, loginStatus } = useUser() const isAuthorized = loginStatus === 'authorized' return ( ) } ``` ### Auth State Listener ```typescript "use client"; export function AuthStateListener() { const { user, loginStatus } = useUser(); const router = useRouter(); useEffect(() => { if (loginStatus === "authorized" && user) { // User just logged in toast.success(`Welcome back, ${user.name || "User"}!`); // Track analytics analytics.identify(user.did, { name: user.name, ethAddress: user.ethAddress, }); // Redirect to dashboard router.push("/dashboard"); } }, [loginStatus, user, router]); useEffect(() => { if (loginStatus === "not-authorized") { // User logged out toast.info("You have been logged out"); // Clear analytics analytics.reset(); // Redirect to home router.push("/"); } }, [loginStatus, router]); return null; // This is a listener component } ``` ### Custom Auth Hook with Additional Logic ```typescript "use client"; interface ExtendedAuthState { user: User | null; isAuthenticated: boolean; isLoading: boolean; hasCompletedProfile: boolean; login: () => void; logout: () => Promise; } export function useUser(): ExtendedAuthState { const auth = useRenownAuth(); const [hasCompletedProfile, setHasCompletedProfile] = useState(false); useEffect(() => { if (auth.user) { // Check if user has completed their profile const isComplete = !!( auth.user.name && auth.user.avatar && auth.user.email ); setHasCompletedProfile(isComplete); } else { setHasCompletedProfile(false); } }, [auth.user]); return { user: auth.user, isAuthenticated: auth.loginStatus === "authorized", isLoading: auth.isLoading, hasCompletedProfile, login: auth.openRenown, logout: auth.logout, }; } ``` ### Role-Based Access Control ```typescript 'use client' interface RBACProps { children: ReactNode allowedRoles?: string[] fallback?: ReactNode } export function RoleBasedAccess({ children, allowedRoles = [], fallback =
Access Denied
}: RBACProps) { const { user, loginStatus } = useUser() if (loginStatus !== 'authorized' || !user) { return fallback } // Check user roles (you'd fetch this from your backend) const userRoles = getUserRoles(user.did) // Implement this const hasAccess = allowedRoles.length === 0 || allowedRoles.some(role => userRoles.includes(role)) return hasAccess ? <>{children} : fallback } // Usage function AdminPanel() { return (

Admin Panel

{/* Admin content */}
) } ``` ## Security Considerations ### 1. Token Storage The SDK stores session data in `sessionStorage` (not `localStorage`) for security: - โœ… Session data clears when tab closes - โœ… Not accessible across tabs - โœ… Not persisted across browser sessions - โœ… Protected from XSS via httpOnly (for API tokens) ### 2. DID Validation Always validate DIDs before processing: ```typescript function isValidDID(did: string): boolean { // Must start with did:pkh: if (!did.startsWith("did:pkh:")) return false; // Must have correct number of parts const parts = did.split(":"); if (parts.length !== 5) return false; // Validate ethereum address format const address = parts[4]; if (!address.match(/^0x[a-fA-F0-9]{40}$/)) return false; return true; } ``` ### 3. Secure Communication Always use HTTPS for Renown URLs: ```typescript const RENOWN_URL = process.env.NEXT_PUBLIC_RENOWN_URL; if (RENOWN_URL && !RENOWN_URL.startsWith("https://")) { console.warn("WARNING: Renown URL should use HTTPS"); } ``` ### 4. Session Timeout Implement session timeout for security: ```typescript const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours function isSessionValid(timestamp: number): boolean { const now = Date.now(); const age = now - timestamp; return age < SESSION_TIMEOUT; } ``` ### 5. Server-Side Verification **Never trust client-side auth alone**. Always verify on the server: ```typescript // Server-side API route export async function GET(request: Request) { const authHeader = request.headers.get("authorization"); if (!authHeader) { return new Response("Unauthorized", { status: 401 }); } // Verify the JWT/credential with Renown const isValid = await verifyRenownCredential(authHeader); if (!isValid) { return new Response("Invalid credentials", { status: 403 }); } // Proceed with authorized request return Response.json({ data: "Protected data" }); } ``` ## Best Practices ### 1. Handle Loading States Always handle loading states to provide good UX: ```typescript function MyComponent() { const { user, isLoading, loginStatus } = useUser() if (isLoading) { return // Show skeleton/spinner } // Now safe to use user/loginStatus } ``` ### 2. Graceful Degradation Provide fallbacks for unauthenticated users: ```typescript function FeatureSection() { const { user } = useUser() return (
{user ? ( ) : ( )}
) } ``` ### 3. Cleanup on Unmount Clean up subscriptions and listeners: ```typescript useEffect(() => { const handleAuthChange = () => { // Handle auth changes }; // Subscribe to auth events const unsubscribe = subscribeToAuthEvents(handleAuthChange); return () => { unsubscribe(); // Cleanup }; }, []); ``` ### 4. Error Boundaries Wrap auth components in error boundaries: ```typescript }> ``` ## Troubleshooting ### Issue: Auth state not updating **Cause:** Component not re-rendering when auth changes **Solution:** Ensure you're using `useUser()` hook, not accessing `window.renown` directly ```typescript // โŒ Wrong const user = window.renown?.user; // โœ… Correct const { user } = useUser(); ``` ### Issue: Session not persisting **Cause:** SessionStorage might be disabled or cleared **Solution:** Check browser settings and handle gracefully ```typescript try { SessionStorageManager.setUserData(data); } catch (error) { console.warn("SessionStorage not available:", error); // Fallback to in-memory storage } ``` ### Issue: Multiple login popups **Cause:** `openRenown()` called multiple times **Solution:** Debounce the login button ```typescript const handleLogin = useCallback( debounce(() => { openRenown(); }, 1000), [openRenown], ); ``` ## Next Steps - Read the [API Reference](../docs/docs/02-APIReference.md) for detailed documentation --- ## API Reference > Source: https://powerhouse.academy/academy/APIReferences/renown-sdk/APIReference Complete API reference for the Renown SDK. ## Table of Contents - [Components](#components) - [Hooks](#hooks) - [Functions](#functions) - [Types](#types) - [Classes](#classes) - [Constants](#constants) ## Components ### `RenownUserProvider` Central authentication provider that automatically initializes the Renown SDK. #### Props ```typescript interface RenownUserProviderProps { children: React.ReactNode; renownUrl?: string; networkId?: string; chainId?: string; loadingComponent?: React.ReactNode; errorComponent?: (error: Error, retry: () => void) => React.ReactNode; } ``` | Prop | Type | Default | Description | | ------------------ | ----------------------------- | ------------------------- | --------------------------------------- | | `children` | `React.ReactNode` | - | **Required.** Child components | | `renownUrl` | `string` | `'https://www.renown.id'` | Renown service URL | | `networkId` | `string` | `'eip155'` | Network identifier | | `chainId` | `string` | `'1'` | Chain identifier | | `loadingComponent` | `React.ReactNode` | undefined | Custom loading UI during initialization | | `errorComponent` | `(error, retry) => ReactNode` | undefined | Custom error UI if initialization fails | #### Example **Basic usage (auto-initializes with defaults):** ```typescript ``` **With custom configuration:** ```typescript

Initializing...

} errorComponent={(error, retry) => (

Failed to initialize

{error.message}

)} > ``` #### Behavior - **Automatically initializes** Renown SDK and ConnectCrypto on mount - Creates global `window.renown` and `window.connectCrypto` instances - Checks sessionStorage for existing sessions - Handles Renown authentication redirects - Shows `loadingComponent` during initialization (if provided) - Shows `errorComponent` if initialization fails (if provided) - If no custom components provided, renders children immediately - Provides auth context to all children --- ### `RenownAuthButton` Smart button component that adapts based on authentication state. #### Props ```typescript interface RenownAuthButtonProps { className?: string; profileBaseUrl?: string; renderAuthenticated?: (props: RenownAuthButtonRenderProps) => React.ReactNode; renderUnauthenticated?: (props: { openRenown: () => void; isLoading: boolean; }) => React.ReactNode; renderLoading?: () => React.ReactNode; showUsername?: boolean; showLogoutButton?: boolean; logoutButtonText?: string; } ``` | Prop | Type | Default | Description | | ----------------------- | ---------- | --------------------------------- | ---------------------------- | | `className` | `string` | `""` | Custom CSS class | | `profileBaseUrl` | `string` | `"https://www.renown.id/profile"` | Base URL for profile | | `renderAuthenticated` | `function` | Default renderer | Custom authenticated state | | `renderUnauthenticated` | `function` | Default renderer | Custom unauthenticated state | | `renderLoading` | `function` | Default renderer | Custom loading state | | `showUsername` | `boolean` | `true` | Show username next to avatar | | `showLogoutButton` | `boolean` | `false` | Show logout button | | `logoutButtonText` | `string` | `"Logout"` | Logout button text | #### Example ```typescript // Basic usage // Custom rendering (
{user.name}
)} renderUnauthenticated={({ openRenown }) => ( )} /> ``` --- ### `RenownLoginButton` A login button with Renown branding. By default, clicking the button triggers login directly. Optionally shows a popover with connect option. #### Props ```typescript interface RenownLoginButtonProps { onLogin: (() => void) | undefined; darkMode?: boolean; style?: CSSProperties; className?: string; renderTrigger?: (props: { onMouseEnter: () => void; onMouseLeave: () => void; isLoading: boolean; }) => ReactNode; showPopover?: boolean; } ``` | Prop | Type | Default | Description | | --------------- | ------------------------- | ------- | ----------------------------------------------------------------------- | | `onLogin` | `() => void \| undefined` | - | Callback when login is requested | | `darkMode` | `boolean` | `false` | Enable dark mode styling | | `style` | `CSSProperties` | - | Custom styles for the button | | `className` | `string` | - | Custom class name | | `renderTrigger` | `function` | - | Custom render function for the trigger button | | `showPopover` | `boolean` | `false` | Show a popover with connect option instead of triggering login directly | #### Example ```typescript // Direct login (default) - clicks trigger onLogin immediately // With popover - shows hover popover with "Connect" button // Dark mode // Custom trigger ( )} /> ``` --- ## Hooks ### `useUser()` Access authentication state and methods. #### Returns ```typescript interface RenownUserContextValue { user: User | null; loginStatus: LoginStatus; isLoading: boolean; isInitialized: boolean; login: (userDid?: string) => Promise; logout: () => Promise; openRenown: () => void; connectCrypto: IConnectCrypto | null; renown: IRenown | null; } ``` | Property | Type | Description | | --------------- | ------------------------ | --------------------------------------------------- | | `user` | `User \| null` | Current authenticated user or null | | `loginStatus` | `LoginStatus` | Current authentication status | | `isLoading` | `boolean` | Whether an auth operation is in progress | | `isInitialized` | `boolean` | Whether the auth system has initialized | | `login` | `function` | Authenticate with optional DID | | `logout` | `function` | Log out current user | | `openRenown` | `function` | Open Renown authentication portal | | `connectCrypto` | `IConnectCrypto \| null` | ConnectCrypto instance for cryptographic operations | | `renown` | `IRenown \| null` | Renown SDK instance | #### Example ```typescript function MyComponent() { const { user, loginStatus, isLoading, login, logout, openRenown, connectCrypto, renown } = useUser() if (isLoading) return
Loading...
if (!user) return return } ``` #### Throws Throws an error if used outside of ``: ``` Error: useUser must be used within a RenownUserProvider ``` --- ## Functions ### `initRenown()` Initialize the Renown SDK. #### Signature ```typescript function initRenown( did: string, networkId: string, renownUrl: string, ): Promise; ``` #### Parameters | Parameter | Type | Description | | ----------- | -------- | ----------------------------------- | | `did` | `string` | Decentralized identifier (DID) | | `networkId` | `string` | Network identifier (e.g., 'eip155') | | `renownUrl` | `string` | Renown service URL | #### Returns `Promise` - Initialized Renown instance #### Example ```typescript const renown = await initRenown( "did:pkh:eip155:1:0x123...", "eip155", "https://www.renown.id", ); ``` --- ### `login()` Authenticate a user with Renown. #### Signature ```typescript function login( userDid: string | undefined, renown: IRenown | undefined, connectCrypto: IConnectCrypto | undefined, ): Promise; ``` #### Parameters | Parameter | Type | Description | | --------------- | ----------------------------- | -------------------------- | | `userDid` | `string \| undefined` | User's DID to authenticate | | `renown` | `IRenown \| undefined` | Renown instance | | `connectCrypto` | `IConnectCrypto \| undefined` | ConnectCrypto instance | #### Returns `Promise` - Authenticated user or undefined #### Side Effects - Stores user session in sessionStorage - Fetches user profile data - Updates auth state #### Example ```typescript const user = await login( "did:pkh:eip155:1:0x123...", window.renown, window.connectCrypto, ); ``` --- ### `logout()` Log out the current user. #### Signature ```typescript function logout(): Promise; ``` #### Returns `Promise` #### Side Effects - Clears sessionStorage - Calls renown.logout() - Removes JWT handler #### Example ```typescript await logout(); ``` --- ### `openRenown()` Open the Renown authentication portal. #### Signature ```typescript function openRenown(): void; ``` #### Returns `void` #### Behavior - Constructs authentication URL with current DID - Adds network and chain parameters - Sets return URL to current location - Redirects to Renown portal #### Example ```typescript function MyLoginButton() { return } // Or use the built-in RenownAuthButton component function Header() { return } ``` --- ### `handleRenownReturn()` Process authentication redirect from Renown. #### Signature ```typescript function handleRenownReturn(): Promise; ``` #### Returns `Promise` #### Behavior - Checks URL for authentication parameters - Extracts user DID from query string - Calls login with the DID - Cleans up URL parameters #### Example ```typescript // Called automatically by UserProvider useEffect(() => { handleRenownReturn(); }, []); ``` --- ### `fetchProfileDataForUser()` Fetch user profile data from Renown API. #### Signature ```typescript function fetchProfileDataForUser(user: User): Promise; ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | --------------------------------------- | | `user` | `User` | User object to enrich with profile data | #### Returns `Promise` - User with profile data #### Behavior - Extracts ETH address from user's DID - Calls Renown profile API - Enriches user object with profile data (name, avatar, etc.) - Returns original user if profile not found #### Example ```typescript const userWithProfile = await fetchProfileDataForUser(user); console.log(userWithProfile.name); // Display name console.log(userWithProfile.avatar); // Avatar URL ``` --- ### `reauthenticateFromSession()` Restore authentication from stored session. #### Signature ```typescript function reauthenticateFromSession(): Promise; ``` #### Returns `Promise` - Restored user or null #### Behavior - Checks for stored session in sessionStorage - Calls login with stored DID - Fetches fresh profile data - Returns null if session invalid or expired #### Example ```typescript const user = await reauthenticateFromSession(); if (user) { console.log("Session restored for:", user.did); } ``` --- ### `extractEthAddressFromDid()` Extract Ethereum address from a DID. #### Signature ```typescript function extractEthAddressFromDid(did: string): string | null; ``` #### Parameters | Parameter | Type | Description | | --------- | -------- | ---------------------------------------------- | | `did` | `string` | DID string (e.g., 'did:pkh:eip155:1:0x123...') | #### Returns `string | null` - Ethereum address or null if invalid #### Example ```typescript const address = extractEthAddressFromDid("did:pkh:eip155:1:0x1234..."); console.log(address); // '0x1234...' ``` --- ## Types ### `User` Represents an authenticated user. ```typescript interface User { did: string; // Decentralized identifier address: string; // Ethereum address name?: string; // Display name from profile email?: string; // Email address avatar?: string; // Avatar image URL ethAddress?: string; // Ethereum address (duplicate of address) } ``` --- ### `LoginStatus` Authentication status enumeration. ```typescript type LoginStatus = | "initial" // Not yet checked | "checking" // Currently checking auth | "authorized" // User is authenticated | "not-authorized"; // User is not authenticated ``` --- ### `RenownUserContextValue` Type for the authentication context value. ```typescript interface RenownUserContextValue { user: User | null; loginStatus: LoginStatus; isLoading: boolean; isInitialized: boolean; login: (userDid?: string) => Promise; logout: () => Promise; openRenown: () => void; connectCrypto: IConnectCrypto | null; renown: IRenown | null; } ``` --- ### `IRenown` Interface for the Renown instance. ```typescript interface IRenown { user: User | undefined | (() => Promise); login: (did: string) => Promise; logout: () => Promise; on: (event: string, handler: Function) => Unsubscribe; } ``` --- ### `IConnectCrypto` Interface for the ConnectCrypto instance. ```typescript interface IConnectCrypto { did: () => Promise; // Additional methods... } ``` --- ## Classes ### `ConnectCrypto` Manages cryptographic operations and DID generation. #### Constructor ```typescript constructor(keyStorage: IKeyStorage) ``` #### Parameters | Parameter | Type | Description | | ------------ | ------------- | -------------------------- | | `keyStorage` | `IKeyStorage` | Key storage implementation | #### Methods ##### `did()` Get the DID for the current key. ```typescript async did(): Promise ``` **Returns:** `Promise` - The DID **Example:** ```typescript const connectCrypto = new ConnectCrypto(new BrowserKeyStorage()); const did = await connectCrypto.did(); console.log(did); // 'did:pkh:eip155:1:0x...' ``` --- ### `BrowserKeyStorage` Browser-based key storage using IndexedDB. #### Constructor ```typescript constructor(); ``` #### Usage ```typescript const keyStorage = new BrowserKeyStorage(); const connectCrypto = new ConnectCrypto(keyStorage); ``` --- ### `SessionStorageManager` Manages user session persistence. #### Static Methods ##### `setUserData()` Store user session data. ```typescript static setUserData(data: { user: User userDid: string loginStatus: LoginStatus timestamp: number }): void ``` **Example:** ```typescript SessionStorageManager.setUserData({ user: currentUser, userDid: currentUser.did, loginStatus: "authorized", timestamp: Date.now(), }); ``` ##### `getUserData()` Retrieve stored user session. ```typescript static getUserData(): { user: User userDid: string loginStatus: LoginStatus timestamp: number } | null ``` **Returns:** Session data or null **Example:** ```typescript const session = SessionStorageManager.getUserData(); if (session) { console.log("Found session for:", session.user.did); } ``` ##### `clearUserData()` Clear stored session. ```typescript static clearUserData(): void ``` **Example:** ```typescript SessionStorageManager.clearUserData(); ``` ##### `isUserDataValid()` Check if session data is valid. ```typescript static isUserDataValid(data: { user: User userDid: string loginStatus: LoginStatus timestamp: number }): boolean ``` **Returns:** `boolean` - Whether session is valid **Example:** ```typescript const data = SessionStorageManager.getUserData(); if (data && SessionStorageManager.isUserDataValid(data)) { // Session is valid } ``` ##### `getStoredUserDid()` Get stored user DID. ```typescript static getStoredUserDid(): string | null ``` **Returns:** `string | null` - Stored DID or null --- ## Constants ### `RENOWN_URL` Default Renown service URL. ```typescript const RENOWN_URL: string = "https://www.renown.id"; ``` --- ### `RENOWN_NETWORK_ID` Default network identifier. ```typescript const RENOWN_NETWORK_ID: string = "eip155"; ``` --- ### `RENOWN_CHAIN_ID` Default chain identifier. ```typescript const RENOWN_CHAIN_ID: string = "1"; ``` --- ## Global Window Extensions The SDK extends the global `Window` interface: ```typescript declare global { interface Window { renown?: IRenown; connectCrypto?: IConnectCrypto; reactor?: { setGenerateJwtHandler: ( handler: (driveUrl: string) => Promise, ) => void; removeJwtHandler: () => void; }; } } ``` ### `window.renown` Global Renown instance after initialization. **Usage:** ```typescript if (window.renown) { const user = await window.renown.login("did:pkh:..."); } ``` ### `window.connectCrypto` Global ConnectCrypto instance after initialization. **Usage:** ```typescript if (window.connectCrypto) { const did = await window.connectCrypto.did(); } ``` --- ## Error Handling ### Common Errors #### `useUser must be used within a RenownUserProvider` **Cause:** Using `useUser()` outside of `` **Solution:** Wrap your component tree with `` ```typescript {/* โœ… Can use useUser */} ``` #### `Invalid DID format` **Cause:** DID doesn't match expected format `did:pkh:networkId:chainId:address` **Solution:** Ensure DID is properly formatted ```typescript // โœ… Valid "did:pkh:eip155:1:0x1234567890123456789012345678901234567890"; // โŒ Invalid "did:1234567890123456789012345678901234567890"; ``` #### `Renown or ConnectCrypto not available` **Cause:** SDK initialization failed **Solution:** The RenownUserProvider automatically initializes the SDK. If you see this error: 1. Check that `` is mounted 2. Check browser console for initialization errors 3. Verify network connectivity to Renown service 4. Try providing an `errorComponent` prop to see detailed error messages ```typescript (

Init failed: {error.message}

)} >
``` --- ## TypeScript Support The SDK is fully typed. Import types as needed: ```typescript User, LoginStatus, RenownUserContextValue, IRenown, IConnectCrypto, } from "@renown/sdk"; ``` --- ## Version Compatibility | SDK Version | React Version | TypeScript Version | | ----------- | ------------- | ------------------ | | 5.x | 18.x - 19.x | 4.5+ | | 4.x | 18.x | 4.5+ | --- ## Related Documentation - [Authentication Guide](../docs/docs/01-Authentication.md) - Comprehensive auth implementation guide --- ## CLI Identity & Authentication > Source: https://powerhouse.academy/academy/APIReferences/renown-sdk/CLIIdentity This guide covers how to authenticate the Powerhouse CLI with your Ethereum identity and use that identity in the Switchboard for authenticated operations with remote services. ## Overview The Powerhouse CLI uses **Renown** for identity management. When you run `ph login`, the CLI: 1. Generates a cryptographic keypair (ECDSA P-256) stored locally 2. Creates a DID (Decentralized Identifier) in `did:key:...` format 3. Opens your browser to authorize this DID to act on behalf of your Ethereum address 4. Stores the authorization credentials for future use This enables the CLI and Switchboard to authenticate with remote drives and services using your Ethereum identity. ## Quick Start ```bash # 1. Login with your Ethereum wallet ph login # 2. Start switchboard with your identity ph switchboard --use-identity ``` ## The Login Command ### Basic Usage ```bash # Authenticate with Renown ph login # Check your authentication status ph login --status # Show only your CLI's DID (useful for scripts) ph login --show-did # Logout and clear credentials ph logout ``` ### How It Works ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ph login โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ 1. CLI generates/loads keypair (.keypair.json) โ”‚ โ”‚ โ””โ”€โ–บ Creates DID: did:key:zDnae... โ”‚ โ”‚ โ”‚ โ”‚ 2. Opens browser to Renown portal โ”‚ โ”‚ โ””โ”€โ–บ URL includes CLI's DID โ”‚ โ”‚ โ”‚ โ”‚ 3. User connects wallet & signs authorization โ”‚ โ”‚ โ””โ”€โ–บ "I authorize did:key:zDnae... to act on my behalf" โ”‚ โ”‚ โ”‚ โ”‚ 4. Renown issues credential โ”‚ โ”‚ โ””โ”€โ–บ Links CLI DID to user's ETH address โ”‚ โ”‚ โ”‚ โ”‚ 5. CLI stores credentials (.auth.json in project dir) โ”‚ โ”‚ โ””โ”€โ–บ Ready to authenticate with remote services โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### Output Example ``` $ ph login Initializing cryptographic identity... CLI DID: did:key:zDnaej4f3d83mmCodYjZyHzUKDSt2dGVKjzD8dd22AS83GtMo Opening browser for authentication... Session ID: a1b2c3d4... Waiting for authentication in browser (timeout in 300 seconds) Please connect your wallet and authorize this CLI to act on your behalf. Waiting................. Successfully authenticated! ETH Address: 0x1234...abcd User DID: did:pkh:eip155:1:0x1234...abcd CLI DID: did:key:zDnaej4f3d83mmCodYjZyHzUKDSt2dGVKjzD8dd22AS83GtMo The CLI can now act on behalf of your Ethereum identity. ``` ## Storage Locations All identity files are stored **per-project** in the current working directory: ``` your-project/ โ”œโ”€โ”€ .keypair.json # CLI's cryptographic keypair (did:key identity) โ”œโ”€โ”€ .auth.json # Authentication credentials (ETH address, User DID, etc.) โ”œโ”€โ”€ powerhouse.config.json โ””โ”€โ”€ ... ``` This means each project can have its own identity and credentials, which is useful for: - Different projects requiring different identities - Team members using the same machine - Separating development and production identities - Isolating credentials between projects ### Environment Variable For CI/CD environments, provide the keypair via environment variable: ```bash # Export keypair as JSON export PH_RENOWN_PRIVATE_KEY='{"publicKey":{"kty":"EC",...},"privateKey":{"kty":"EC",...}}' # Now ph login --show-did will use this keypair ph login --show-did ``` ## Using Identity in Switchboard ### Starting with Identity ```bash # Enable identity using keypair from ph login ph switchboard --use-identity # Output includes identity DID: # โžœ Switchboard: http://localhost:4001 # โžœ Identity: did:key:zDnaej4f3d83mmCodYjZyHzUKDSt2dGVKjzD8dd22AS83GtMo ``` ### Identity Options | Option | Description | | ----------------------- | ------------------------------------- | | `--use-identity` | Enable identity using `.keypair.json` | | `--keypair-path ` | Use a custom keypair file | | `--require-identity` | Fail if no keypair exists | ### Requiring Identity Use `--require-identity` when the switchboard must have a valid identity: ```bash # Fails if no keypair exists (user must run ph login first) ph switchboard --require-identity # Error if not logged in: # Error: Identity required but failed to initialize. Run "ph login" first. ``` ### Custom Keypair Path ```bash # Use a specific keypair file ph switchboard --use-identity --keypair-path /path/to/my-keypair.json ``` ## How the Switchboard Uses Identity When the Switchboard starts with identity enabled, it can: 1. **Authenticate with Remote Drives**: Generate bearer tokens for API requests 2. **Sign Operations**: Cryptographically sign document operations 3. **Identify Itself**: Present its DID to remote services ### Getting Bearer Tokens The Switchboard can generate bearer tokens for authenticated API calls: ```typescript // Get the switchboard's DID const did = await getConnectDid(); console.log("Switchboard DID:", did); // Get a bearer token for a remote drive const token = await getBearerToken("https://remote.drive.example.com"); console.log("Bearer Token:", token); // Use in API requests const response = await fetch("https://remote.drive.example.com/api/documents", { headers: { Authorization: `Bearer ${token}`, }, }); ``` ## Security Considerations ### Identity Files Protection The `.keypair.json` and `.auth.json` files contain sensitive data. Protect them: ```bash # Add to .gitignore echo ".keypair.json" >> .gitignore echo ".auth.json" >> .gitignore # Set restrictive permissions (Unix) chmod 600 .keypair.json .auth.json ``` ### CI/CD Best Practices For automated environments: 1. **Use environment variables** instead of files 2. **Store secrets securely** (GitHub Secrets, AWS Secrets Manager, etc.) 3. **Rotate keys** periodically 4. **Limit scope** - use separate identities for different environments ```yaml # GitHub Actions example jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup identity env: PH_RENOWN_PRIVATE_KEY: ${{ secrets.PH_KEYPAIR }} run: | ph switchboard --use-identity & ``` ### Authorization Scope When you authorize the CLI's DID: - It can act on behalf of your Ethereum address - It can authenticate with services that trust Renown - It **cannot** sign Ethereum transactions or access your wallet ## Troubleshooting ### "No existing keypair found" ```bash $ ph switchboard --require-identity Error: Identity required but failed to initialize. Run "ph login" first. ``` **Solution**: Run `ph login` to create a keypair: ```bash ph login # Then retry ph switchboard --require-identity ``` ### "Authentication timed out" The browser authentication didn't complete in time. **Solutions**: - Increase timeout: `ph login --timeout 600` - Check browser opened correctly - Ensure you completed the wallet connection ### Different DID than expected Each project directory has its own `.keypair.json`. **Check current DID**: ```bash ph login --show-did ``` **Use specific keypair**: ```bash ph switchboard --use-identity --keypair-path ~/.shared-keypair.json ``` ## API Reference ### Login Options | Option | Type | Default | Description | | -------------- | ------- | ------------------------------ | ---------------------- | | `--renown-url` | string | `https://renown.powerhouse.io` | Renown server URL | | `--timeout` | number | `300` | Auth timeout (seconds) | | `--logout` | boolean | `false` | Clear credentials | | `--status` | boolean | `false` | Show auth status | | `--show-did` | boolean | `false` | Print DID only | ### Switchboard Identity Options | Option | Type | Default | Description | | -------------------- | ------- | --------------- | -------------------- | | `--use-identity` | boolean | `false` | Enable identity | | `--keypair-path` | string | `.keypair.json` | Keypair file path | | `--require-identity` | boolean | `false` | Fail without keypair | ### Exported Functions (Switchboard) ```typescript // Get the ConnectCrypto instance function getConnectCrypto(): IConnectCrypto | null; // Get the switchboard's DID async function getConnectDid(): Promise; // Get a bearer token for a remote URL async function getBearerToken( driveUrl: string, address?: string, refresh?: boolean, ): Promise; ``` ## Related Documentation - [Renown SDK Overview](../docs/docs/00-Overview.md) - Introduction to Renown - [Authentication Guide](../docs/docs/01-Authentication.md) - Web app authentication - [API Reference](../docs/docs/02-APIReference.md) - Full SDK reference --- # Component Library ## Document-Engineering > Source: https://powerhouse.academy/academy/ComponentLibrary/DocumentEngineering The reusable components in the Document-Engineering system are a set of of front-end components based on graphQL scalars. Powerhouse also has a set of custom scalars that are not part of the graphQL standard but are specific to the web3 ecosystem. These components are offered through the **Powerhouse Document-Engineering system** with the help of storybook & the Academy documentation. It provides a collection of pre-built, reusable UI components designed for consistency and efficiency across Powerhouse applications and editors. Think of it as a toolkit of standard UI elements like buttons, inputs, and checkboxes with many of these components based on GraphQL scalars. **INFO:** A GraphQL scalar is essentially a primitive, indivisible value in the GraphQL type system. Here are the key points to understand: - **Basic Building Blocks:** Scalars are the basic data typesโ€”like String, Int, Float, Boolean, and IDโ€”that represent atomic values. - **Leaf Nodes:** Scalars are the "leaves" of a GraphQL query. They can't have any sub-fields, meaning once you hit a scalar in a query, that's the final value. - **Custom Scalars:** Besides the built-in scalars, you can define custom scalars (e.g., a Date type) if you need to handle more specific formats or validations. Powerhouse does this specific for the web3 ecosystem. ::: ## What are Components? In the context of Powerhouse Builder platform, components can be thought of as reusable elements, or ready-to-use building blocks that help builders implement **document editors & viewers** with little to no effort. An important utility aspect of a component is that it serves its users as a **data input field**, providing structured ways to enter and manipulate information within your document models. ## Document Editors vs Document Viewers Understanding the relationship between document editors and viewers is crucial for component usage: **Document Editor**: A specific document type that is used by one or more users to make data entries and update its state. Key utility is the ability to enter data in a structured format, making it a great tool for collaboration within a group of authorized users. **Document Viewer**: Does not allow modifications. It's a great way to inform about the state of the document type, making it a great tool for providing a broader group or public with transparent insights. Document viewers do not have to match the view of the editor one-to-one - the data presented could be framed as a specific selection, or filtered to provide desired insights. **TIP:** The same component that will be used in a document viewer will have a **disabled state** (not allowed to edit documents). Document editors precede document viewers - you would start by creating a document editor and then, if needed, decide which viewer format is useful. ## Scalars vs. General UI Components ### Scalar Components Scalars are here to help you define custom fields in your document model schema and speed up the development process. There are two applications of scalar components in the document model workflow: 1. At the **schema definition** level where you build your schema and write your GraphQL state schema. 2. At the **frontend / react** level where you import it and place it in your UI to represent the scalar field These are specialized form components, each corresponding to a GraphQL scalar type (e.g., String, Number, Boolean, Currency, PHID). They are built on top of react-hook-form, offering out-of-the-box validation but must be wrapped with a Form component in order to work properly. **Location:** @powerhousedao/document-engineering/scalars https://github.com/powerhouse-inc/document-engineering **Key Feature**: Must be used within a Form component provided by this library. ### General-Purpose UI Components This category includes a broader range of UI elements such as simplified versions of the Scalar components (which don't require a Form wrapper but lack built-in validation), as well as other versatile components like Dropdown, Tooltip, Sidebar, ObjectSetTable and more. These are designed for crafting diverse and complex user interfaces. **Location:** @powerhousedao/document-engineering/ui https://github.com/powerhouse-inc/document-engineering ## Component Types Classification Inspired by atomic design methodology, Powerhouse classifies components into the following categories: ### Fragment The smallest element that combined together makes up a scalar or other simple component. **Examples:** Character counter, Checkbox field, Label ### Scalar (Simple Component) The simplest component that contains the basic input field for one-dimensional data type (single value). **Examples:** Integer, Boolean, String, Powerhouse ID (PHID) ### Complex Component Compound component that has an object/array value. It's made up of multiple scalars combined to serve a specific function. **Examples:** Sidebar (tree structure navigation component with content-style navigation for hierarchical data) ### Layout Component Purpose-specific container for other components like lists of other components, color layouts, sections, etc. **Examples:** Homepage section layout **INFO:** The Powerhouse team is building a Component library with a wide range of components embedding best UX practices & key functionality. This library establishes standards and best practices for building documents while fast-tracking the building process through facilitation of the most basic & useful component types. ## Component Behavior & UX Principles Besides the ability to input data, components have another crucial utility: they describe the mechanism of user interaction through implementing a defined set of behavior rules. **Best Practices for Component Behavior:** - Implementing behaviors at a component level is much more efficient than at the document level - Good component behavior feels natural to the user and is easily understood - Components should be intuitive and not require additional tutorials or explanations - Start with the most simple/basic behaviors first, then layer additional behaviors on top - Keep behaviors as simple as needed - less is more ## Exploring Components with Storybook We use Storybook as an interactive catalog for our design system components. It allows you to visually explore each component, interact with different states, and understand how to integrate them into your projects. [https://storybook.powerhouse.academy](https://storybook.powerhouse.academy) **Understanding the Storybook Interface:** 1. **Visual Demo:** The main panel shows the rendered component (e.g., a `Checkbox`). You can interact with it directly to see different states (checked, unchecked, disabled). 2. **Usage Snippet:** Below the demo, you'll typically find a basic code example demonstrating how to include the component in your code (e.g., ``). This provides a starting point for implementation. 3. **Props Table:** Further down, a table lists the properties (`props`) the component accepts. Props are like settings or configuration options. For the `Checkbox`, this table would show props like `label`, `defaultValue`, `value`, `onChange`, etc., often with descriptions of what they control. ## **Storybook vs. Source Code:** Storybook serves as essential documentation and a usage guide. Our developers write Storybook "stories" to demonstrate components and document their common props. However, the **ultimate source of truth** for a component's capabilities is its actual source code (e.g., the `.tsx` file within the `@powerhousedao/document-engineering/scalars` package). While Storybook aims for accuracy, there might occasionally be discrepancies or undocumented props. ## Implementing a Component Let's walk through the typical workflow for using a component from the document-engineering system, using `BooleanField` for a checkbox in the [TodoList editor](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors). 1. **Identify the Need:** While building your feature (e.g., the TodoList editor), you determine the need for a standard UI element, like a checkbox to mark items as complete. 2. **Consult the Document Engineering Components in Storybook:** - Open the Powerhouse Storybook instance. [https://storybook.powerhouse.academy](https://storybook.powerhouse.academy) - Navigate or search to find the `BooleanField` component (used for checkboxes). - Review the visual examples and interactive demo. - Examine the "Usage" snippet and the **Props table** to understand the basic implementation and available configuration options (`name`, `value`, `onChange`, etc.). 3. **Import the Component:** In your code editor, open the relevant file (e.g., `editors/todo-list-editor/components/Checkbox.tsx`). Add an import statement at the top to bring the component into your file's scope: ```typescript import { Form, BooleanField, } from "@powerhousedao/document-engineering/scalars"; ``` This line instructs the build process to locate the `Form` and `BooleanField` components within the installed `@powerhousedao/document-engineering/scalars` package and make them available for use. :::info Form Wrapper Required Scalar components like `BooleanField` must be wrapped in a `Form` component from the same package. This provides built-in validation and form state management. ::: 4. **Use and Configure the Component:** Place the component tag in your JSX where needed. Use the information from Storybook (usage snippet and props table) as a guide, but adapt the props to your specific requirements: **Step 4a: Create a reusable Checkbox component** ```typescript // editors/todo-list-editor/components/Checkbox.tsx import { Form, BooleanField } from "@powerhousedao/document-engineering/scalars"; interface CheckboxProps { value: boolean; onChange: (value: boolean) => void; } export const Checkbox = ({ value, onChange }: CheckboxProps) => { return (
{}}> ); }; ``` **Step 4b: Use it in your Todo component with the document model hook** ```typescript // editors/todo-list-editor/components/Todo.tsx import { useSelectedTodoListDocument, updateTodoItem } from "todo-tutorial/document-models/todo-list"; import type { TodoItem } from "todo-tutorial/document-models/todo-list"; import { Checkbox } from "./Checkbox.js"; type Props = { todo: TodoItem }; export function Todo({ todo }: Props) { // Use the hook to get dispatch function const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; return (
{ dispatch(updateTodoItem({ id: todo.id, checked: !todo.checked, })); }} /> {todo.text}
); } ``` You configure the component's appearance and behavior by passing the appropriate values to its props. Note the use of the `useSelectedTodoListDocument` hook to access the dispatch function. 5. **Test and Refine:** Run your application (e.g., using `ph connect`) to see the component in context. Verify its appearance and functionality. ## Usage The Document Engineering package provides several entry points for different use cases in your powerhouse project: ### Main Package ```typescript ``` ### UI Components ```typescript ``` ### Scalars For data manipulation and transformation utilities: ```typescript ``` ### GraphQL For GraphQL related utilities and schema definitions: ```typescript ``` ### Styles To include the package's styles: ```typescript ``` ## Import Maps Within the project, the following import maps are available: - `#assets` - Assets utilities and components - `#scalars` - Scalar transformations and utilities - `#ui` - UI components - `#graphql` - GraphQL related utilities Please don't hesitate to reach out in our discord channels with any questions. Happy designing! ### Up next: Create Custom Scalars You can learn how to do so in our guide on [Creating Custom Scalars](/academy/ComponentLibrary/CreateCustomScalars). --- ## Step 1: Create Custom Scalars > Source: https://powerhouse.academy/academy/ComponentLibrary/CreateCustomScalars This tutorial provides step-by-step instructions for creating custom scalars & components, and to contributing to the document-engineering project. The github repo for the Document-Engineering can be found [here](https://github.com/powerhouse-inc/document-engineering/tree/main) ### Creating New GraphQL Scalars GraphQL scalars are custom data types that define how data is validated, serialized, and parsed. This guide will walk you through creating a new scalar in the `src/scalars/graphql/` directory. ## Step 1: Create the Scalar File Create a new TypeScript file in `src/scalars/graphql/` for your scalar. Use `EmailAddress.ts` as a reference. **Example: Creating a `PhoneNumber.ts` scalar** ```typescript GraphQLError, GraphQLScalarType, type GraphQLScalarTypeConfig, Kind, } from "graphql"; export interface ScalarType { input: string; output: string; } export const type = "string"; // TS type in string form export const typedef = "scalar PhoneNumber"; export const schema = z .string() .regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format"); export const stringSchema = 'z.string().regex(/^\\+?[1-9]\\d{1,14}$/, "Invalid phone number format")'; const phoneValidation = (value: unknown): string => { if (typeof value !== "string") { throw new GraphQLError(`Value is not string: ${JSON.stringify(value)}`); } const result = schema.safeParse(value); if (result.success) return result.data; throw new GraphQLError(result.error.message); }; export const config: GraphQLScalarTypeConfig = { name: "PhoneNumber", description: "A field whose value conforms to international phone number format.", serialize: phoneValidation, parseValue: phoneValidation, parseLiteral: (value) => { if (value.kind !== Kind.STRING) { throw new GraphQLError( `Can only validate strings as phone numbers but got a: ${value.kind}`, { nodes: value }, ); } return phoneValidation(value.value); }, }; export const scalar = new GraphQLScalarType(config); ``` ### Key Components to Update: 1. **`type`**: The TypeScript type (usually `'string'` for text-based scalars) 2. **`typedef`**: The GraphQL type definition (e.g., `'scalar PhoneNumber'`) 3. **`schema`**: Zod validation schema for your data type 4. **`stringSchema`**: String representation of the zod schema (used for code generation) 5. **Validation function**: Custom validation logic for your scalar 6. **`config.name`**: The name of your scalar (must match the typedef) 7. **`config.description`**: Human-readable description of the scalar ## Step 2: Register the Scalar in `scalars.ts` After creating your scalar file, you need to register it in `src/scalars/graphql/scalars.ts`. This involves updating multiple sections of the file. The github repo for the Document-Engineering can be found [here](https://github.com/powerhouse-inc/document-engineering/tree/main) ### 2.1 Add Namespace Import Add your scalar to the namespace imports section (around line 2): ```typescript // namespace imports -- DO NOT REMOVE OR EDIT THIS COMMENT // ... other imports ... ``` ### 2.2 Add Type Export Add the type export (around line 22): ```typescript // export types -- DO NOT REMOVE OR EDIT THIS COMMENT export type { ScalarType as AmountScalarType } from "./Amount.js"; // ... other type exports ... export type { ScalarType as PhoneNumberScalarType } from "./PhoneNumber.js"; // ADD THIS LINE export type { ScalarType as URLScalarType } from "./URL.js"; ``` ### 2.3 Add to Export Object Add your scalar to the main export object (around line 40): ```typescript export { Amount, AmountCrypto, // ... other exports ... PhoneNumber, // ADD THIS LINE URLScalar, }; ``` ### 2.4 Add to Custom Scalars Add your scalar to the `customScalars` object (around line 54): ```typescript export const customScalars: Record> = { // ... other scalars ... PhoneNumber, // ADD THIS LINE URLScalar, } as const; ``` #### 2.5 Add to Resolvers Add your scalar to the `resolvers` object (around line 74): ```typescript export const resolvers = { // export resolvers -- DO NOT REMOVE OR EDIT THIS COMMENT AmountTokens: AmountTokens.scalar, // ... other resolvers ... PhoneNumber: PhoneNumber.scalar, // ADD THIS LINE Amount: Amount.scalar, }; ``` ### 2.6 Add to Type Definitions Add your typedef to the `typeDefs` array (around line 90): ```typescript export const typeDefs = [ // export typedefs -- DO NOT REMOVE OR EDIT THIS COMMENT AmountTokens.typedef, // ... other typedefs ... PhoneNumber.typedef, // ADD THIS LINE Amount.typedef, ]; ``` ### 2.7 Add to Generator Type Definitions Add your scalar to the `generatorTypeDefs` object (around line 105): ```typescript export const generatorTypeDefs = { // export generator typedefs -- DO NOT REMOVE OR EDIT THIS COMMENT [AmountTokens.config.name]: AmountTokens.type, // ... other entries ... [PhoneNumber.config.name]: PhoneNumber.type, // ADD THIS LINE [Amount.config.name]: Amount.type, }; ``` ### 2.8 Add to Validation Schema Add your scalar to the `validationSchema` object (around line 120): ```typescript export const validationSchema = { // export validation schema -- DO NOT REMOVE OR EDIT THIS COMMENT [AmountTokens.config.name]: AmountTokens.stringSchema, // ... other entries ... [PhoneNumber.config.name]: PhoneNumber.stringSchema, // ADD THIS LINE [Amount.config.name]: Amount.stringSchema, }; ``` ## Step 3: Create Tests for Your Scalar Every scalar must have comprehensive tests to ensure it works correctly. Create a test file in `src/scalars/graphql/test/` following the naming convention `YourScalar.test.ts`. **Example: Creating `PhoneNumber.test.ts`** ```typescript describe("PhoneNumber Scalar", () => { it("should serialize a phone number", () => { const phoneNumber = "+1234567890"; expect(scalar.serialize(phoneNumber)).toBe(phoneNumber); }); it("should throw an error if the value is not a string", () => { const phoneNumber = 123; expect(() => scalar.serialize(phoneNumber)).toThrow(); }); it("should throw an error if the value is not a valid phone number", () => { const phoneNumber = "invalid-phone"; expect(() => scalar.serialize(phoneNumber)).toThrow(); }); it("should parse a valid phone number", () => { const phoneNumber = "+1234567890"; expect(scalar.parseValue(phoneNumber)).toBe(phoneNumber); }); it("should throw an error if parse a value that is not a valid phone number", () => { const phoneNumber = "invalid-phone"; expect(() => scalar.parseValue(phoneNumber)).toThrow(); }); it("should throw an error if parse a value that is not a string", () => { const phoneNumber = 123; expect(() => scalar.parseValue(phoneNumber)).toThrow(); }); it("should parse a valid phone number from a literal", () => { const phoneNumber = "+1234567890"; expect( scalar.parseLiteral({ kind: Kind.STRING, value: phoneNumber, }), ).toBe(phoneNumber); }); it("should throw an error if parse a literal that is not a valid phone number", () => { const phoneNumber = "invalid-phone"; expect(() => scalar.parseLiteral({ kind: Kind.STRING, value: phoneNumber, }), ).toThrow(); }); it("should throw an error if parse a literal that is not a string", () => { const phoneNumber = "+1234567890"; expect(() => scalar.parseLiteral({ kind: Kind.INT, value: phoneNumber, }), ).toThrow(); }); }); ``` #### Required Test Cases Your scalar tests should cover these essential scenarios: ##### Serialization Tests - โœ… **Valid values**: Test that valid inputs are serialized correctly - โŒ **Invalid types**: Test that non-string inputs throw errors - โŒ **Invalid format**: Test that strings not matching your validation throw errors ##### Parse Value Tests - โœ… **Valid values**: Test that valid inputs are parsed correctly - โŒ **Invalid format**: Test that invalid strings throw errors - โŒ **Invalid types**: Test that non-string inputs throw errors ##### Parse Literal Tests - โœ… **Valid STRING literals**: Test that valid string literals are parsed correctly - โŒ **Invalid STRING literals**: Test that invalid string literals throw errors - โŒ **Non-STRING literals**: Test that non-string literal kinds (INT, FLOAT, etc.) throw errors #### Testing Best Practices 1. **Test edge cases**: Include boundary values and common invalid inputs 2. **Test multiple valid formats**: If your scalar accepts different valid formats, test them all 3. **Use descriptive test names**: Make it clear what each test is validating 4. **Follow the naming convention**: `YourScalar.test.ts` in the `test/` directory #### Example Edge Cases for Different Scalar Types **String-based scalars (like PhoneNumber):** ```typescript // Test empty string expect(() => scalar.parseValue("")).toThrow(); // Test too long/short values expect(() => scalar.parseValue("123")).toThrow(); expect(() => scalar.parseValue("+" + "1".repeat(20))).toThrow(); // Test special characters expect(() => scalar.parseValue("+1-234-567-890")).not.toThrow(); ``` **Number-based scalars:** ```typescript // Test zero expect(scalar.parseValue(0)).toBe(0); // Test negative numbers expect(() => scalar.parseValue(-1)).toThrow(); // Test decimal numbers expect(scalar.parseValue(123.45)).toBe(123.45); ``` **Date-based scalars:** ```typescript // Test valid ISO date expect(scalar.parseValue("2023-12-25T00:00:00Z")).toBe("2023-12-25T00:00:00Z"); // Test invalid date format expect(() => scalar.parseValue("25/12/2023")).toThrow(); ``` ## Step 4: Validate Your Implementation After implementing your scalar and tests, make sure to: 1. **Run the tests** to ensure they all pass 2. **Build the project** to ensure there are no TypeScript errors 3. **Test GraphQL queries** that use your new scalar 4. **Verify code generation** works with your new scalar ### Common Scalar Types Here are some common patterns for different types of scalars: #### String-based Scalars ```typescript export const type = "string"; export const schema = z.string().min(1).max(100); ``` #### Number-based Scalars ```typescript export const type = "number"; export const schema = z.number().positive(); ``` #### Date-based Scalars ```typescript export const type = "string"; export const schema = z.string().datetime(); ``` **INFO:** **Contributing and UI for Scalars** - **Open Source**: Please submit contributions as a pull request to the Powerhouse team. - **UI is Optional but Helpful**: A design or UI for your scalar isn't required, but it helps reviewers understand its purpose. - **Semantic Scalars**: Some scalars don't need a unique UI. For instance, `Title` and `Description` might both use a simple text input but serve a semantic role by adding specific meaning and validation to the schema. ::: ### Tips - Always follow the naming convention: use PascalCase for scalar names - Include meaningful validation in your Zod schema - Write clear, descriptive error messages - Keep the `stringSchema` in sync with your `schema` definition - Test edge cases in your validation function - Update all required sections in `scalars.ts` --- ## Step 2: Integrate Your Scalar into a React Component > Source: https://powerhouse.academy/academy/ComponentLibrary/IntegrateIntoAReactComponent This guide explains how to use a custom scalar (created as described in the previous step) within a React component. You'll learn how to leverage the scalar's validation schema for form input, display errors, and ensure a seamless user experience. ## Table of Contents - [Overview](#overview) - [Step 1: Import the Scalar and Dependencies](#step-1-import-the-scalar-and-dependencies) - [Step 2: Define Component Props](#step-2-define-component-props) - [Step 3: Implement the Component](#step-3-implement-the-component) - [Step 4: Render the Input and Error](#step-4-render-the-input-and-error) - [Step 5: Example Usage](#step-5-example-usage) - [Best Practices](#best-practices) - [Tips](#tips) ## Overview Custom scalars provide type-safe validation and parsing for your data. Integrating them into React components ensures that user input is validated consistently with your backend and schema definitions. This is especially useful for form fields like email, phone number, Ethereum address, etc. ## Step 1: Import the Scalar and Dependencies Import your scalar and React hooks. You may use any input component to capture user input. In the following example, `FormInput` is used for demonstration purposes, but you can use a standard ``, a custom component, or any UI library input. ```typescript // FormInput is just an example. You can use any input component you prefer. ``` Replace `EthereumAddress` with your scalar's name as needed. ## Step 2: Define Component Props Define the props for your component. Typically, you'll want an `onChange` callback to notify the parent of the value and its validity: ```typescript export interface EthereumAddressProps { onChange?: (address: string, isValidAddress: boolean) => void; } ``` Adapt the prop names and types to your scalar (e.g., `PhoneNumberProps`, `onChange(phone: string, isValid: boolean)`). ## Step 3: Implement the Component Use React state to track the input value. Use the scalar's Zod schema for validation. Call `onChange` with the value and validity whenever the input changes. > **Note:** The input component in this example is `FormInput`, but you can use any input or UI component to capture user input. The key is to validate the value using the scalar's schema. ```typescript export const EthereumAddress: React.FC = ({ onChange }) => { const [address, setAddress] = useState(""); // Validate using the scalar's Zod schema const result = EthereumAddressScalar.schema.safeParse(address); const errors = result.error?.errors.map((error) => error.message).join(", "); // Notify parent of value and validity if (onChange) { onChange(address, result.success); } return (
{/* Replace FormInput with any input component you prefer */} setAddress(e.target.value)} placeholder="0x...." aria-label="Ethereum address input" />
); }; ``` **Key Points:** - Use the scalar's `.schema.safeParse(value)` for validation. - Display error messages if validation fails. - Call `onChange` with both the value and its validity. - Use accessible labels and attributes. - The input component is flexibleโ€”use what fits your UI best. ## Step 4: Render the Input and Error - Use any form input component (e.g., `FormInput`, ``, or a custom UI input) for the field. - Show error messages below the input when validation fails. - Add accessibility attributes (`aria-label`, `htmlFor`). ## Step 5: Example Usage Here's how you might use your component in a parent form: ```typescript { // Handle the address and its validity console.log("Address:", address, "Valid:", isValid); }} /> ``` Replace `EthereumAddress` with your scalar component as needed. ## Best Practices - **Validation:** Always use the scalar's schema for validation to ensure consistency with your backend. - **Accessibility:** Use proper labels, `aria-*` attributes, and keyboard navigation. - **Error Handling:** Display clear, user-friendly error messages. - **DRY Principle:** Reuse the scalar's schema and avoid duplicating validation logic. - **Type Safety:** Use TypeScript types for props and state. ## Tips - Keep your UI clean and intuitive. - Sync your component with any updates to the scalar's schema. - Test edge cases (empty input, invalid formats, etc.). - Use meaningful placeholder text and labels. - Consider supporting additional props (e.g., `disabled`, `required`) for flexibility. --- # Architecture ## Powerhouse Architecture > Source: https://powerhouse.academy/academy/Architecture/PowerhouseArchitecture **Vetra is part of the Powerhouse Ecosystem** and acts as the builder platform for creating an independent, open-source and decentralized back-end for any SaaS, ERP, CMS or CRM needs. ## Local First. Built to Scale. Vetra helps you build any type of web application on a **Reactive Document Architecture**. Define workflows once, deploy them globally, and co-own the software you create. The architecture is built on a minimal but powerful tech-stack: **GraphQL**, **TypeScript**, and **React**. ### Reactive Document Architecture The Powerhouse framework is built around structured document models and declarative design: - **Reactive**: Real-time, responsive, and message-driven with an elastic scalable architecture. - **Document-Centric**: Documents as local-first, self-contained data structures and nodes in a decentralized network. - **Git-like UX**: State-of-the-art editing with history branching, merging, and commenting. - **Stateful**: Documents with well-defined operations as state transitions become mini-APIs. - **Scalable**: CQRS and EDA-inspired architecture with read models for data aggregation. ### Specification Driven Design & Development Complementing the Reactive Document Architecture, Powerhouse embraces **Specification Driven Design & Development**. This approach enables you to communicate your solution and intent through structured specification documents designed for AI collaboration. Specifications serve as a shared languageโ€”enabling precise, iterative edits that turn messy intent into clean execution. Our documents are machine-readable and executable, laying the groundwork for a "Git for Intent" for your AI agents. Therefore, Vetra is the stack for sovereign infrastructure and AI-run, AI-mediated, AI-executed business, organizations, and networks. ## Target Audiences Vetra supports a variety of organizations with a headless open-source back-end: - Decentralized Autonomous Organizations (DAOs) - Scalable Network Organizations (an evolution of DAOs within the Powerhouse framework) - NGOs and Governmental Organizations - Cooperatives and distributed teams ## Host Applications The Powerhouse ecosystem makes use of 4 core host applications that together form a modular, scalable operating system for any organization or business. Each application serves a distinct role, yet they are interdependentโ€”working as a unified system to streamline operations, enhance collaboration, and drive automation. 1. **Connect** โ€“ A collaboration hub and private workspace for independent contributors. 2. **Switchboard** โ€“ The data infrastructure and API engine for managing remote instances. 3. **Fusion** โ€“ An SDK for visualizing collected data and public-facing collaboration. 4. **Renown** โ€“ A decentralized reputation and identity system. ## How It All Connects: Reactors The host applications sync their documents with one another through **Reactors**. A reactor is a node within any Powerhouse network responsible for storing documents, resolving conflicts, and verifying document event histories. Reactors can be configured for: - **Local Storage** โ€“ For offline or on-device access. - **Cloud Storage** โ€“ For centralized, scalable data management. - **Decentralized Storage** โ€“ Such as Ceramic or IPFS for distributed storage. The documents within the network react to one another through the **DocSync** protocolโ€”which sends updates from one reactor to another, ensuring all data stays synchronized across the system. ![Powerhouse Host Apps Interaction](../docs/docs/images/PowerhouseArchitecture.png) ## Powerhouse Platforms With the help of these host applications, Powerhouse is launching 2 platforms that demonstrate the infrastructure that can be built with the Powerhouse tech-stack: - [**Vetra Builder Platform**](https://vetra.io/): Sovereign infrastructure for scalable network organizationsโ€”local first, built to scale. - [**Achra Decentralized Operations Platform**](https://achra.com/): Where open organizations can procure and purchase services. --- ## Working with the Reactor > Source: https://powerhouse.academy/academy/Architecture/WorkingWithTheReactor **TIP:** Document models are the common design pattern that is used for all documents and files. DocSync is a decentralized synchronization protocol that is storage agnostic. **Document Models** are _what_ is synced and **DocSync** is _how_ document models are synced. But who is doing the syncing? We call these participants **Reactors**. ### Powerhouse Reactors **What is a Reactor?** Powerhouse Reactors are the nodes in the network that store documents, resolve conflicts and rerun operations to verify document event histories. Reactors can be configured for local storage, centralized cloud storage or on a decentralized storage network. A Reactor is essentially a storage node used in Powerhouse's framework to handle documents and traditional files. It supports multiple storage solutions, including: - **Local Storage**: For offline or on-device access. - **Cloud Storage**: For centralized, scalable data management. - **Decentralized Storage**: Such as Ceramic or IPFS, enabling distributed and blockchain-based storage options. ### Core Functions of Reactors - **Data Synchronization**: Reactors ensure that all data, whether local or distributed, remains up-to-date and consistent across the system. - **Modular Storage Adapters**: They support integration with different storage backends depending on organizational needs. - **Collaboration Support**: Reactors facilitate document sharing and peer-to-peer collaboration across contributors within the network. **TIP:** The DocSync protocol _sends updates from one reactor to another_ - **smashing document operations into one another** - to ensure all data is synced. A **reactor** is responsible for storing data and resolving merge conflicts. Editing data and submitting new operations must be done through Powerhouse's native applications (Connect, Switchboard, Fusion). Each instance of these applications contains a Reactor that is responsible for storing data and syncing data through DocSync. In other words, Powerhouse applications are how Reactors can be accessed, manipulated, steered, visualized and modified. A local Connect desktop application's reactor can therefore sync with the Reactor of a remote drive (e.g. Switchboard instance). Powerhouse Storage Layer ### Why Are Reactors Important? They are key to ensuring the scalability and resilience of decentralized operations. By acting as the backbone for document models in the Powerhouse framework, they enable seamless version control and event-driven updates. Reactors provide the foundation for advanced features like real-time collaboration, history tracking, and decentralized audits. This modular, flexible infrastructure enables organizations to build efficient and robust decentralized systems, tailored for modern network organizations ## The ReactorClient API The `IReactorClient` is the primary interface for interacting with a reactor programmatically. It wraps lower-level APIs to provide a simpler, Promise-based interface for document operations. ```typescript ``` **INFO:** `@powerhousedao/reactor-browser` re-exports all reactor types for convenience in browser environments (editors, drive-apps, subgraphs). If you are working outside the browser โ€” for example in a standalone Node.js script, CLI tool, or server-side processor โ€” import directly from `@powerhousedao/reactor`. ### Reading documents | Method | Description | | ------------------------------------------------------------------------------ | ------------------------------------------------- | | `get(identifier, view?)` | Retrieve a document by id or slug | | `getOutgoingRelationships(sourceIdentifier, relationshipType, view?, paging?)` | List documents related from a source | | `getIncomingRelationships(targetIdentifier, relationshipType, view?, paging?)` | List documents related into a target | | `find(search, view?, paging?)` | Search documents by type, parentId, ids, or slugs | | `getOperations(documentIdentifier, view?, filter?, paging?)` | Retrieve operations for a document | | `getDocumentModelModules(namespace?, paging?)` | List registered document model modules | | `getDocumentModelModule(documentType)` | Get a specific document model module | The optional `ViewFilter` lets you target a specific branch, set of scopes, or revision: ```typescript type ViewFilter = { branch?: string; scopes?: string[]; revision?: number; }; ``` The `SearchFilter` lets you narrow results: ```typescript type SearchFilter = { type?: string; parentId?: string; ids?: string[]; slugs?: string[]; }; ``` All list methods support pagination via `PagingOptions` (`{ cursor, limit }`) and return `PagedResults` with a `next()` helper for fetching the next page. ### Writing documents | Method | Description | | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `create(document, parentIdentifier?)` | Create a document from a full `PHDocument` object | | `createEmpty(documentModelType, options?)` | Create an empty document of a given type | | `createDocumentInDrive(driveId, document, parentFolder?)` | Create a document inside a drive in a single batched operation | | `execute(documentIdentifier, branch, actions)` | Apply actions and wait for completion | | `executeAsync(documentIdentifier, branch, actions)` | Submit actions and return immediately with a `JobInfo` | | `rename(documentIdentifier, name, branch?)` | Rename a document | | `addRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch?)` | Add a typed relationship between two documents | | `removeRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch?)` | Remove a typed relationship between two documents | | `moveRelationship(sourceParent, targetParent, targetIdentifier, relationshipType, branch?)` | Move a relationship from one source to another | | `deleteDocument(identifier, propagate?)` | Delete a document (`PropagationMode.Cascade` deletes children too) | | `deleteDocuments(identifiers, propagate?)` | Bulk delete | ### Subscribing to changes ```typescript const unsubscribe = reactorClient.subscribe( { type: "powerhouse/todo-list" }, // SearchFilter (event) => { // event.type is one of: Created, Deleted, Updated, // ParentAdded, ParentRemoved, ChildAdded, ChildRemoved console.log(event.type, event.documents); }, ); ``` ### Job tracking Write operations return `JobInfo` objects. A job tracks the lifecycle of a set of actions as they move through the reactor. ```typescript const job = await reactorClient.executeAsync(docId, "main", actions); const completed = await reactorClient.waitForJob(job.id); ``` You can also poll with `getJobStatus(jobId)`. For the full API reference, see [IReactorClient API Reference](/academy/APIReferences/ReactorClient). ## Job lifecycle Every mutation in the reactor is processed as a **job**. Jobs move through these statuses: ``` PENDING โ†’ RUNNING โ†’ WRITE_READY โ†’ READ_READY โ†˜ FAILED ``` | Status | Meaning | | ------------- | --------------------------------------------------------------------- | | `PENDING` | Job is queued but not yet started | | `RUNNING` | Job is currently being executed by the reducer | | `WRITE_READY` | Operations have been written to the operation store | | `READ_READY` | All read models have finished processing โ€” document is fully readable | | `FAILED` | Job encountered an unrecoverable error | Only `READ_READY` and `FAILED` are terminal statuses. The `execute()` method on `IReactorClient` waits until `READ_READY` before resolving; `executeAsync()` returns immediately with a `JobInfo` at `PENDING`. ## Reactor event system The reactor uses an internal event bus to coordinate between subsystems. Events are grouped into three categories: ### Core job events (`ReactorEventTypes`) | Event | Numeric ID | When it fires | | ----------------- | ---------- | --------------------------------------------- | | `JOB_PENDING` | 10001 | Job is registered and waiting to execute | | `JOB_RUNNING` | 10002 | Job starts executing | | `JOB_WRITE_READY` | 10003 | Operations are written to the operation store | | `JOB_READ_READY` | 10004 | All read models have finished processing | | `JOB_FAILED` | 10005 | Job failed with an unrecoverable error | ### Sync events (`SyncEventTypes`) | Event | Numeric ID | When it fires | | -------------------------- | ---------- | ------------------------------------------------ | | `SYNC_PENDING` | 20001 | Sync operations are queued in outboxes | | `SYNC_SUCCEEDED` | 20002 | All sync operations for a job succeed | | `SYNC_FAILED` | 20003 | At least one sync operation failed | | `DEAD_LETTER_ADDED` | 20004 | A sync operation is moved to dead letter storage | | `CONNECTION_STATE_CHANGED` | 20005 | Remote connection state changes | ### Queue events (`QueueEventTypes`) | Event | Numeric ID | When it fires | | --------------- | ---------- | --------------------------------------- | | `JOB_AVAILABLE` | 10000 | Queue has work available for processing | ## Configuring your reactor In addition to the choice of storage, Reactors also have other configurations. - The **operational data** and **read models** associated with the document models inside a reactor allow to query the gathered data inside a document model or quickly visualize the crucial insights at a glance. - **Processors** are components that receive operations and perform side effects โ€” analytics tracking, relational database indexing, webhooks, and more. You register processor factories with the reactor, and it automatically creates processor instances for each drive. The processor pipeline works as follows: 1. **Operations are written** โ€” a job completes its write phase, persisting operations to storage 2. **Pre-ready read models update** โ€” built-in read models like `DocumentView` and `DocumentIndexer` update their state 3. **`JOB_READ_READY` event fires** โ€” signaling that the document is fully readable 4. **Post-ready read models update** โ€” the `ProcessorManager` routes matching operations to user-defined processors via `onOperations()` For a step-by-step guide to building processors, see [Building a Processor](/academy/MasteryTrack/WorkWithData/BuildingAProcessor). ### Ordering guarantees - **Global ordinal**: Every operation gets a monotonically increasing `ordinal` in its `OperationContext`, enabling cross-document ordering - **Within a processor**: Operations arrive sorted by ordinal (chronological order) - **Between processors**: Processors for the same drive execute in parallel โ€” there is no inter-processor ordering guarantee - **Per-document serialization**: The queue serializes execution per document, even across scopes and branches - **Catch-up on restart**: Processors automatically replay missed operations after a restart (each processor's progress is tracked via the `ProcessorCursor` table) If you are working with the Reactor directly or need additional information regarding its architecture you can visit: https://github.com/powerhouse-inc/powerhouse/blob/main/packages/reactor/docs/ARCHITECTURE.md --- # Book of Powerhouse ## Overview > Source: https://powerhouse.academy/bookofpowerhouse/Overview Powerhouse is an initiative at the intersection of software development, legal innovation, and new business models for open-source projects. These elements form a common vision for reimagining how software is created, funded, and sustained. While each component has its own purpose, they are deeply interconnected and designed to support one another.
Book of Powerhouse Outline: 1. General Framework and Philosophy 2. Powerhouse Software Architecture 3. Technical Development Approaches 4. SNOs as an extension of DAOs 5. The Powerhouse Operations Platform --- ## **Part 1: Powerhouse General Framework and Open-Source Capitalism** > Source: https://powerhouse.academy/bookofpowerhouse/GeneralFrameworkAndPhilosophy Powerhouse emerged in the wake of Ethereumโ€™s DAO movement, which began gaining momentum after the collapse of _The DAO_ in 2016. This turning point spurred the creation of blockchain-enabled organizations capable of operating without centralized corporate structures. Over the years, governance systems and decentralized operational tools have advanced, paving the way for the next generation of organizations across chains like Solana, Ethereum Layer 2 solutions, and beyond. Powerhouse aims to advance these principles and build large, global organizations around networks. Powerhouse operates at the intersection of open-source innovation and decentralized coordination. This section outlines the philosophical foundation and systemic structure that guides its operations and visionโ€”what we call โ€œOpen-Source Capitalism.โ€ ## **Why Open-Source Capitalism?** Capitalism is the most powerful system of economic coordination ever invented. It incentivizes productivity, innovation, and risk-taking. But just as importantโ€”open-source has proven to be one of the most powerful engines of innovation within that system. Open-source unlocks coordination across boundaries: between companies, communities, and individuals. It builds shared infrastructure at unprecedented speed, allows bottom-up experimentation, and lowers barriers to participation. This is why Big Tech, for all its closed platforms and walled gardens, has embraced open-source at the core of its own operationsโ€”releasing projects like React, Kubernetes, and TensorFlow not out of charity, but because open collaboration is often the best way to build foundational technology. Yet this dynamic is incomplete. While open-source has dominated infrastructure, it has consistently struggled to capture valueโ€”particularly in consumer applications and services. The common assumption is that the challenge is distribution: how to equitably allocate funding, resources, and decision-making. But Open-Source Capitalism takes a broader view. Before we can redistribute value, we have to make sure value is being captured in the first place. Thatโ€™s the central promise of Open-Source Capitalism: not just to build open alternatives, but to make them self-sustaining, investable, and scalable. Itโ€™s about combining the pro-market, pro-innovation ethos of open-source with incentive structures that actually workโ€”so we can build networks that donโ€™t just survive, but thrive. ## **Four Principles of Open-Source Capitalism** 1. **Coordination Through Marketplace Platforms** Open-Source Capitalism demands new types of platformsโ€”public marketplaces where contributors, customers, and investors can interact transparently. Like Uber or Airbnb, these marketplaces optimize for liquidity and coordination, but with open governance and ownership. \ 2. **Incentive Alignment via Tokens and Revenue Sharing** Powerhouse uses Proof of Work Tokens (POWTs) to compensate contributors fairly over time, enabling deferred compensation models that align long-term incentives. \ 3. **Reinvestment Through Revenue-Generating Hubs** Monetized OSS must funnel returns back to the contributors. The RGH (Revenue Generating Hub) manages licensing, sales, and commercial transactions in alignment with decentralized values. \ 4. **Make Open Source Investable** By enabling investor flows through the Operational Collateral Fund (OCF), open-source projects gain sustainable funding channels. Returns are tied to the downstream use of open infrastructure. \ ## **Decentralized Operations and Collaboration** Open-Source Capitalism doesnโ€™t work without new organizational structures. DAOs promised a way forward, but fell short: plagued by coordination failures, incentive misalignment, and lack of clarity around roles, responsibilities, and execution. Powerhouse emerged from this gapโ€”building systems that retain the openness of decentralized networks while introducing the operational rigor of traditional institutions. At the core of this approach is a belief in structured decentralization. Powerhouse enables independent teams to coordinate around shared goals, using workflows encoded in software rather than enforced by hierarchy. Contributors take ownership over workstreams, reputations are built over time, and budgets are allocated based on transparent processes. This is not decentralization for its own sakeโ€”it is a functional reimagining of how work gets done at scale. Transparency plays a critical role. Rather than relying on opaque decision-making or internal politics, Powerhouse makes contributions, decisions, and payments legible. Governance and compensation are traceable across time, creating a persistent and auditable record of organizational behavior. The result is a system that fosters trust not just between individuals, but across entire networks. This is what makes Powerhouse unique: itโ€™s not just about running decentralized softwareโ€”itโ€™s about running decentralized organizations. Open-Source Capitalism is the economic philosophy. Scalable Network Organizations are the structure. And these principles come to life through the operational frameworks that guide every action on the network. --- ## Part 2: Powerhouse Software Architecture > Source: https://powerhouse.academy/bookofpowerhouse/PowerhouseSoftwareArchitecture Powerhouseโ€™s software architecture is designed to empower scalable, decentralized organizations with a modular, layered approach. It uses document models as the core unit that used across three layers: - the **Data Infrastructure Layer**, which handles secure storage and synchronization across local, cloud, and decentralized systems; - the **Host Application Layer**, which provides contributor tools like Powerhouse Connect and Fusion; - and the Customization **Layer**, enabling bespoke solutions for specific use cases. ## Software Architecture ### Document Models - A **document model** is a structured representation of data and workflows that captures relationships, states, and operations within a system. By treating documents as dynamic entities that evolve over time, document models allow multiple people to contribute, make changes, and track progress in a flexible and transparent way. Document models are central to Powerhouseโ€™s development approach, forming the backbone of adaptable workflows and versatile data management. In environments where contributors work asynchronously and systems must respond to evolving needs, document models provide the flexibility and responsiveness that traditional, static structures often lack. - Building on this foundation, Powerhouseโ€™s document models are designed to go beyond static data storage, encapsulating the complex relationships, states, and operations that drive decentralized workflows. Each document functions as a living entity, continuously evolving to reflect updates and interactions among contributors. This allows for: - **Asynchronous Collaboration**: Contributors can work on documents without being bound by linear processes. For example, a task document can move through different states (e.g., draft, review, approved) based on specific triggers or events. - **Customizable Processes**: Document models are highly adaptable, supporting unique workflows for different teams or organizations. This flexibility ensures that systems can address specialized requirements while maintaining overall consistency and transparency. - **Event-Driven Updates**: By integrating with Powerhouseโ€™s event-driven architecture, documents can respond to real-time changes, such as new data inputs or status updates, ensuring workflows remain efficient and responsive. ### Three layers of Powerhouse 1. **Data Infrastructure Layer -** This layer is responsible for data storage and synchronization, whether locally, in the cloud, or on decentralized storage networks like Ceramic or IPFS. It includes: - **Reactor**: A storage node for documents and files, supporting multiple storage adapters for various environments. - **Powerhouse Network Components**: services required to build scalable networks of Reactor nodes, such as event buses, queues, caching services, and load balancers. 2. **Host Application Layer - t**his layer provides the tools to build apps and platforms using modular host applications. Each host application is an empty shell that gains functionality through plugins. The key host applications include: - **Powerhouse Connect**: Tools for contributors to perform their roles with tailored plugins for document management. - **Powerhouse Switchboard**: A scalable API service for aggregating data into operational, analytical, or other specialized read models. - **Powerhouse Fusion**: Public-facing collaboration platforms or marketplaces for the SNOโ€™s platform economy. - **Powerhouse Renown**: Decentralized authentication and reputation management for contributors. - **Powerhouse Academy**: Onboarding and training tools for contributors, offering tutorials and reputation badges. 3. **Customization Layer - t**his layer consists of organization-specific plugins and instances of the host applications. It enables customization and deployment of unique platforms for each network organization. Examples include: - **Connect Plugins**: Tailored apps for contributors. - **Switchboard Plugins**: Aggregated data and API services. - **Fusion Plugins**: Custom marketplaces and user-facing platforms. - **Renown and Academy Plugins**: Contributor reputation systems and training resources. --- ## Part 3: Development Approaches > Source: https://powerhouse.academy/bookofpowerhouse/DevelopmentApproaches Powerhouseโ€™s development approaches are designed to enable efficient, scalable, and innovative solutions for decentralized organizations. By drawing from blockchain principles, adopting Model-Driven Development (MDD), leveraging dynamic document models, and employing a Rapid Application Development (RAD) process, Powerhouse provides a comprehensive framework to address the unique challenges of decentralized systems. These methodologies enhance collaboration, streamline workflows, and accelerate iteration cycles, empowering teams to design, implement, and adapt their solutions seamlessly while staying aligned with Powerhouseโ€™s vision for sustainable and decentralized growth. ### Blockchain principles applied to decentralized operations - Powerhouseโ€™s development approach draws inspiration from the principles of blockchain technology while adapting them to meet the specific needs of decentralized organizations. These principles heavily influence its architecture, blending blockchainโ€™s strengths with practical adaptations for scalable operations. - Powerhouse shares several core ideas with blockchain: - **Immutability**: Like a blockchain, Powerhouse emphasizes immutable data records. Through event sourcing, every state change is preserved as an append-only event, ensuring transparent, auditable histories. - **Decentralization**: Both systems operate without relying on a centralized authority. Powerhouseโ€™s Document Synchronization Protocol (DocSync) does not rely on a centralized server but can run P2P. It is also storage agnostic, so there is no lock-in. Users have the option to rely on decentralized, centralized or local storage solutions. - **Transparency**: Inspired by the visibility of blockchain transactions, Powerhouse ensures operational workflows and changes are traceable, which makes governance and accountability easier. - While Powerhouse builds on blockchain principles, it diverges in key ways to address operational challenges: - **Local Scalability**: Blockchainโ€™s global consensus can limit speed and scalability. Powerhouse prioritizes local-first workflows, ensuring contributors can operate efficiently without requiring global synchronization. - **Tailored Operations**: Unlike blockchainโ€™s general-purpose design, Powerhouseโ€™s architecture focuses on organizational needs, supporting custom workflows, dynamic governance, and flexible scaling. ### Model-Driven Development & Rapid Application Development (RAD) - Powerhouse leverages **Model-Driven Development (MDD)** and **Rapid Application Development (RAD)** to streamline the creation of scalable, decentralized systems. - MDD enables cross-functional collaboration by using **abstract models** as blueprints for workflows, data, and system behaviorsโ€”similar to how GraphQL schemas unify API design. These models ensure alignment across teams, enable early validation, and support seamless system evolution. **Meta-models** extend this by automating code generation, documentation, and workflow updates, reducing manual effort and ensuring consistency. - RAD accelerates development by eliminating backend complexity and leveraging **automated code generation**. Pre-built frameworks and reusable components let developers focus on UI/UX without managing backend logic. An **event-driven architecture** handles state and data sync, while document models serve as a **source of truth**, ensuring APIs, data structures, and front-end scaffolding stay in sync. Developers can rapidly iterate on tailored user experiencesโ€”akin to โ€œswapping skinsโ€ in a gameโ€”without disrupting core functionality. ### CQRS (Command Query Responsibility Segregation) - CQRS, or Command Query Responsibility Segregation, is a key design principle in Powerhouseโ€™s software architecture. It separates the responsibilities of handling write operations (commands) and read operations (queries) into distinct models, optimizing both for their specific purposes. - In the Powerhouse framework: - **Commands** handle operations that modify the state, such as creating or updating documents. These commands are validated and stored as events, forming the immutable history central to the systemโ€™s transparency and auditability. - **Queries** are optimized for retrieving data, leveraging read models designed for performance, scalability, and flexibility. These models aggregate data from events to provide users with tailored insights, whether for analytics, search, or operational tasks. - This separation offers significant advantages: 1. **Scalability**: Write and read models can scale independently, allowing Powerhouse to support large decentralized organizations without performance bottlenecks. 2. **Maintainability**: By isolating business logic (commands) from query logic, developers can iterate on one without affecting the other, ensuring the system evolves efficiently. 3. **Flexibility**: Powerhouse supports multiple types of read modelsโ€”such as relational databases, full-text search, and analyticsโ€”each tailored to specific organizational needs. ### Event-driven Architectures (EDA) - Event-Driven Architecture (EDA) is a foundational element of Powerhouseโ€™s software philosophy, designed to create responsive, scalable systems that support asynchronous workflows. In EDA, events represent meaningful changes or actions, such as โ€œDocument Updatedโ€ or โ€œContributor Added,โ€ and these events trigger reactions across the system in real time. - EDA enables Powerhouse to design systems that are inherently responsive and scalable. By decoupling components, EDA allows systems to react to events independently, ensuring high availability and performance even as complexity grows. For example, when a document is updated, different processes like validation, notifications, and analytics generation can run in parallel without blocking each other. This architecture makes Powerhouse systems highly efficient in handling distributed operations, as it allows them to scale dynamically by adding or replicating components where needed. It also improves fault tolerance, as failures in one part of the system do not cascade to others due to the loosely coupled design. - **Asynchronous Workflows -** EDA is particularly effective in supporting asynchronous workflows, which are critical for decentralized organizations. In this model, components communicate through events, eliminating the need for direct dependencies. This loose coupling enables workflows to operate flexibly, allowing different tasks to be executed simultaneously without blocking others. For instance, when a contributor submits work, separate processesโ€”like task approval, logging, and analyticsโ€”can run independently, ensuring smoother coordination. Additionally, event histories can be replayed to debug workflows or audit past operations, providing transparency and traceability essential to decentralized systems. --- ## Part 4: Scalable Network Organizations (SNOs) > Source: https://powerhouse.academy/bookofpowerhouse/SNOsandANewModelForOSSandPublicGoods Decentralized Autonomous Organizations (DAOs) once promised to revolutionize global collaboration by enabling decentralized governance, transparent decision-making, and equitable ownership. However, the reality has often fallen short. DAOs have struggled with legal ambiguity, resource allocation inefficiencies, and broken incentive structures. These challenges have stifled their ability to scale and compete with centralized organizations. Scalable Network Organizations (SNOs) emerge as the next evolutionary step. Combining the principles of decentralization with the operational rigor of traditional organizations, SNOs provide a structured framework for decentralized governance, sustainable operations, and global scalability. Powerhouseโ€™s vision for SNOs aims to deliver on the original promise of DAOs, empowering organizations to scale without sacrificing their decentralized principles. ### Core Components of SNOs[โ€‹](https://staging.powerhouse.academy/docs/bookofpowerhouse/SNOsandANewModelForOSSandPublicGoods#core-components-of-snos) At the heart of Scalable Network Organizations (SNOs) are five entities, each fulfilling a critical role in scaling decentralized operations. From governance to funding and IP management, these components work together to ensure alignment, collaboration, and financial sustainability. - DAO - The DAO is the governing entity responsible for setting the strategic vision and ensuring alignment across the SNO. Through on-chain governance mechanisms powered by smart contracts, the DAO facilitates transparent decision-making, distributed ownership, and accountability. Members participate in proposing and voting on budgets, initiatives, and key operational decisions, creating a decentralized system where authority is widely distributed rather than concentrated in a single entity. - By maintaining immutable records and eliminating the need for intermediaries, the DAO supports scalable and adaptable governance structures that suit decentralized networks. Its ability to flexibly adapt rules and processes ensures that the SNO remains efficient and innovative as it grows, while retaining the values of transparency and inclusivity. - Operational Hub (OH) - The Operational Hub acts as the administrative core of the SNO, handling contributor relationships, compliance, and payment processing. By leveraging legal structures like Swiss Associations, the OH ensures contributors are protected from liability while enabling the SNO to interact with banks, vendors, and regulatory entities. This hub simplifies complex operations such as tax reporting, cross-border compliance, and contributor agreements, removing these burdens from individual participants. - The Operational Hub acts as the administrative core of the SNO, handling contributor relationships, compliance, and payment processing. By leveraging legal structures like Swiss Associations, the OH ensures contributors are protected from liability while enabling the SNO to interact with banks, vendors, and regulatory entities. This hub simplifies complex operations such as tax reporting, cross-border compliance, and contributor agreements, removing these burdens from individual participants. - Operational Collateral Fund (OCF) - The OCF provides the financial infrastructure to fuel the SNOโ€™s growth. It allocates resources to high-potential initiatives and rewards contributors by issuing Proof of Work Tokens (POWTs). These tokens align incentives by linking compensation to measurable contributions, ensuring that individuals and teams are motivated to create long-term value. POWTs represent a stake in the success of the network, making contributors invested in the outcomes of the projects they support. - Beyond its internal role, the OCF also attracts external investment by offering structured opportunities for funding impactful projects within the network. By maintaining transparency in its allocation processes and linking funding to measurable outputs, the OCF builds trust among both contributors and investors, creating a virtuous cycle where capital supports meaningful innovation. - Revenue Generating Hub (RGH) - The RGH is the SNOโ€™s commercial interface, enabling the network to generate sustainable revenue while maintaining its decentralized principles. It manages licensing agreements, product sales, and customer-facing activities, ensuring that all revenue-generating operations align with the broader goals set by the DAO. By addressing the regulatory complexities associated with handling fiat revenue or contractual obligations, the RGH provides a seamless bridge between decentralized networks and traditional market systems. - The RGH supports multiple revenue streams, including enterprise licensing for proprietary versions of open-source software, subscription models for SaaS offerings, and consulting services tailored to client needs. A portion of the revenue collected flows back into the ecosystem, funding operations, rewarding contributors through the OCF, and sustaining the open-source projects that drive the SNOโ€™s long-term success. - IP-Holding Entity (IPSPV) - The IP-Holding Entity safeguards the SNOโ€™s intellectual property assets, including trademarks, copyrights, and other IP rights. It is responsible for enforcing licensing agreements and ensuring that the use of the networkโ€™s IP aligns with governance decisions made by the DAO. By employing a dual licensing model, the IPSPV supports open-source availability through copyleft licenses while generating revenue from enterprise licenses, allowing businesses to use proprietary versions of the SNOโ€™s software. - The IPSPV also plays a critical role in protecting the networkโ€™s creative assets from exploitation or infringement. It ensures that the IP is managed as a collective resource, aligned with the communityโ€™s mission and values, while providing a steady revenue stream for reinvestment into the ecosystem. By separating IP management from operational activities, the IPSPV ensures that the SNO remains focused on innovation without losing control of its most valuable assets. ### Legal Foundations for SNOs[โ€‹](https://staging.powerhouse.academy/docs/bookofpowerhouse/SNOsandANewModelForOSSandPublicGoods#legal-foundations-for-snos) Legal structures are crucial for the success of SNOs, providing clarity, compliance, and protection. - Multisig Participation Agreements (MPAs) \ MPAs are a foundational legal tool within the SNO framework. These agreements define the roles and responsibilities of multisig wallet signers, ensuring accountability and mitigating internal liability risks. By formalizing governance processes, MPAs reduce the chaos and inefficiency often associated with decentralized decision-making. They also protect contributors by clarifying liability boundaries, preventing signers from inadvertently exposing themselves to legal risks. \ MPAs are fully customizable, allowing SNOs to adapt them to their specific operational needs. They integrate seamlessly with Gnosis Safe wallets, creating a transparent and secure environment for managing resources. - Swiss Associations \ Swiss Associations offer a flexible and cost-effective legal wrapper for early-stage SNOs. These entities provide liability protection, separate legal personhood, and the ability to engage in commercial activities that support the SNOโ€™s mission. Notably, Swiss Associations do not require registration, preserving privacy while maintaining compliance. Their reputation as part of Switzerlandโ€™s crypto-friendly regulatory environment makes them ideal for DAOs transitioning into SNOs. These are typically used as legal structures for the OCR and IPSPV, while the OH and RGH may be Swiss foundations but their jurisdiction is likely determined by where inbound and outbound payments are taking place. - Open Source Legal Templates \ Powerhouse has developed a library of open-source legal templates to simplify the setup and operation of SNO entities. These templates include Contributor Agreements, MPAs, and licensing contracts, reducing the complexity and cost of navigating legal requirements. By standardizing these processes, Powerhouse enables SNOs to focus on innovation and growth. --- ## **Part 5: Powerhouse Platforms โ€“ Decentralized Operations and Builder** > Source: https://powerhouse.academy/bookofpowerhouse/SNOsInActionAndPlatformEconomies ## **Introduction** The Powerhouse architecture is not only organizational but also deeply technological. To enable scalable network organizations (SNOs) to operate effectively, Powerhouse has developed two core platforms that provide the infrastructure for decentralized coordination and execution: the **Decentralized Operations Platform** and the **Builder Platform**. These platforms are complementary: one structures and stabilizes daily operations; the other opens up participation and innovation. Together, they form the digital substrate of the Powerhouse model, encoding its governance logic, collaboration structures, and incentive mechanisms directly into software. --- ## **Decentralized Operations Platform** The Decentralized Operations Platform serves as the operational engine of a SNO. It provides the workflows, rules, and execution logic required for contributors to collaborate without a central management layer. This includes systems for compensation, budgeting, IP management, and contributor reputation. At its core, the platform acts as a programmable coordination system. Contributors are onboarded, assigned tasks, compensated, and recognized through transparent, rule-based processes encoded in smart contracts and synchronized document models. These workflows are not static: they evolve based on activity, inputs, and contributor feedback, adapting to the changing needs of the network. The Decentralized Operations Platform also embeds accountability. Actions taken on the platform generate verifiable recordsโ€”both financial and reputationalโ€”that form a shared source of truth. Disputes can be resolved, work can be audited, and contributors can prove their track record across projects and teams. This persistent memory allows SNOs to grow while retaining coherence and trust. Strategically, the platform supports multiple service categories, including governance operations, legal and financial services, and compliance. Each of these is modular, and the marketplace of service providers enables networks to plug in what they need, when they need it. Revenue is structured around project-based transactions and reinforced by policies that encourage on-platform fulfillment, ensuring alignment across stakeholders. --- ## **Builder Platform** While the Decentralized Operations Platform governs execution, the Builder Platform governs creation. It is designed for extending the Powerhouse architectureโ€”enabling developers and contributors to build new tools, workflows, and modules that others in the ecosystem can use. The Builder Platform represents a shift in how coordination infrastructure is developed. Instead of building monolithic applications, contributors define document types, schemas, automation rules, and interfaces as reusable modules. These are published to a shared registry, where they can be discovered, forked, extended, or monetized. Every time a module is used in another networkโ€™s deployment, its original authors receive a share of the value generated. In this way, Powerhouse makes open-source infrastructure economically sustainable and creates a new incentive model for public goods. Technically, the Builder Platform is integrated with the rest of the Powerhouse stack. Its outputs are interoperable with the Operations Platform, the Governance layer, and the contributor onboarding systems. It uses typed schemas, CLI scaffolding, and standardized packaging to ensure modules are composable and production-ready. Strategically, the platform ensures that innovation remains decentralized. No single team or organization controls what can or cannot be built. Any contributor with sufficient context and intent can extend the systemโ€”and be rewarded for doing so. This model transforms Powerhouse from a static platform into a living ecosystem. --- ## **A Unified Infrastructure Layer** Together, these platforms operationalize the vision of scalable, decentralized networks. The Operations Platform provides the scaffolding for work: roles, rules, payments, and projects. The Builder Platform enables the ecosystem to evolve: by building, sharing, and monetizing new capabilities. They are not products to be soldโ€”they are foundational infrastructure for a new kind of organization. Powerhouse is not simply offering software; it is building the operating system for a post-corporate world. --- # Release Notes ## Powerhouse v5.3.0 ๐Ÿš€ > Source: https://powerhouse.academy/academy/ReleaseNotes/v5.3.0 ## โœจ Highlights 1. **Authentication & Permissions** - CLI authentication and document-level permissions 2. **Improved Code Generation** - TS Morph and templates for faster, more reliable generation 3. **Runtime Document Model Subgraphs** - No more generated subgraph code to manage This release focuses on authentication, permissions, improved code generation, and runtime document model subgraphs. ## ๐Ÿ” CLI Authentication New commands for authentication workflows: | Command | Description | | ----------------- | --------------------------------------------- | | `ph login` | Authenticate with your Powerhouse identity | | `ph access-token` | Generate access tokens for API authentication | The CLI now supports full authentication workflows for secure operations and programmatic API access. ## ๐Ÿ›ก๏ธ Document Permission Service Fine-grained access control at the document level for Switchboard. - **Operation Permissions** - Control who can perform specific operations - **Document Group Permissions** - Organize documents with shared access rules - **Feature Flag** - Enable/disable via configuration When enabled, all document operations are validated against permission rules. Enterprise-grade access control that integrates seamlessly with existing Switchboard deployments. ## ๐Ÿ“Š Autogenerated Document Model Subgraphs Subgraphs are now **automatically available** on Switchboard at runtime. ### โš ๏ธ Action Required Previously generated subgraphs should be **deleted** to avoid conflicts. This eliminates manual subgraph generation and maintenance. Clean up old generated files to prevent runtime conflicts. ## โš›๏ธ New React Hooks `useGetDocument` and `useGetDocuments` ```typescript const getDocument = useGetDocument(); const onDocumentSelected = async (id: string) => { const document = await getDocument(id); // Fetch on demand }; ``` ## โš›๏ธ Dispatch Callbacks Optional callbacks to handle results of dispatched actions. ```typescript const [document, dispatch] = useDocumentById(documentId); dispatch( myAction, (errors) => { // Handle errors (e.g., show toast notification) alert(errors); }, (document) => { // Handle success console.log("Document updated:", document); }, ); ``` Useful for showing toast notifications or triggering follow-up actions based on dispatch results. ## โš›๏ธ React Hooks Bug Fixes - Hooks returning multiple documents now correctly update when any document changes - Improved React Suspense integration to avoid unnecessary loading states Thank you **Liberuum**! These improvements make working with documents more reliable and flexible. ## ๐ŸŽจ Document Model Editor Improvements Enhanced state editing experience in the Document Model Editor. The Document Model Editor now provides a better experience for editing document state schemas with improved validation and feedback. ## ๐Ÿ› ๏ธ Improved Code Generation Faster, more reliable code generation with **TS Morph** and **templates**. - Templates are easier to maintain and customize - More predictable output - Better error messages The new template-based approach makes codegen more maintainable and gives developers clearer error messages when something goes wrong. ## ๐Ÿ“ Updated Editor Boilerplate Better starting point for custom editors with improved default styling. The new boilerplate includes DocumentToolbar integration, proper state management with hooks, and cleaner default styles. ## ๐Ÿ”„ Migration Steps 1. **Run migrations** - `ph migrate` 2. **Update editor styles** - Editors now control their own padding 3. **Delete document model subgraphs** - Remove generated subgraphs to avoid conflicts 4. **Update config files** - Add `vitest.config.ts` to `tsconfig.json` exclude and ESLint's `allowDefaultProject` Follow these steps carefully for a smooth upgrade to v5.3.0. ## ๐Ÿ“š Documentation Coming soon to **https://academy.vetra.io** - Reactor API Authorization - Document Permission System - Inspector Modal guide - Updated hooks documentation - Vetra Studio usage guides --- # Miscellaneous ## Cookbook > Source: https://powerhouse.academy/academy/Cookbook ## Powerhouse CLI recipes This section covers recipes related to the `ph-cmd`, the command-line tool for Powerhouse project initialization, code generation, package management, and running local development environments.
Installing 'ph-cmd' ### How to install Powerhouse CLI --- ### Problem statement You need to install the Powerhouse CLI (`ph-cmd`) to create and manage Powerhouse projects. ### Prerequisites - Node.js 24 installed - pnpm package manager 10 installed - Terminal or command prompt access ### Solution ### Step 1: Install the CLI globally ```bash pnpm install -g ph-cmd ``` ### Step 2: Verify the installation ```bash ph-cmd --version ``` ### Optional: Install specific versions ```bash # For the staging version pnpm install -g ph-cmd@staging # For a specific version pnpm install -g ph-cmd@ ``` ### Expected outcome - Powerhouse CLI (`ph-cmd`) installed globally on your system - Access to all Powerhouse CLI commands for project creation and management ### Common issues and solutions - Issue: Permission errors during installation - Solution: Use `sudo` on Unix-based systems or run as administrator on Windows - Issue: Version conflicts - Solution: Clean your system using the uninstallation recipe before installing a new version ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Setting up or Resetting the Global Powerhouse Configuration](#setting-up-or-resetting-the-global-powerhouse-configuration) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
Managing ph-cmd Versions and Package Information ### How to Switch Between ph-cmd Versions --- ### Problem statement You need to switch to a different version of `ph-cmd`, check available versions, or understand how to manage package versions effectively. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - Terminal or command prompt access - Package manager (pnpm, npm, or yarn) installed ### Solution ### Using Version Tags and Specific Versions To change to a different version of `ph-cmd`, reinstall it globally with your package manager: ```bash # Install latest version with a specific tag npm install -g ph-cmd@staging pnpm install -g ph-cmd@dev # Install a specific version number npm install -g ph-cmd@1.2.3-staging.10 pnpm install -g ph-cmd@6.0.0-dev.33 ``` **Important:** Always use the same package manager you used for the original global install to avoid conflicting installations. ### Checking Your Current Installation Use the `which` command to see where your global install is located: ```bash which ph # Example output: /Users/username/Library/pnpm/ph ``` This shows which package manager was used (in this case, pnpm). ### Viewing Available Versions Use `npm view` to see all available versions and tags for any package: ```bash npm view ph-cmd ``` This displays: - Current version information - Available dist-tags (latest, dev, staging, test) - Specific version numbers for each tag - Package maintainers and publish information **Example dist-tags output:** ``` dist-tags: latest: 5.3.0 dev: 6.0.0-dev.33 staging: 5.3.0-staging.24 test: 2.5.0-test.0 ``` ### Expected outcome - Ability to switch between different versions of `ph-cmd` - Understanding of available version tags and specific versions - Knowledge of how to check current installation and available versions ### Best Practices - Use specific version numbers (e.g., `ph-cmd@6.0.0-dev.33`) instead of tags when you need to ensure exact version consistency - Check `npm view ph-cmd` before switching to see the latest available versions - Remember that without specifying a version, `@latest` is installed by default ### Common issues and solutions - Issue: Conflicting installations or commands not working - Solution: Ensure you use the same package manager for all global installs. Use `which ph` to verify your installation location. - Issue: Outdated version after installation - Solution: Use `npm view ph-cmd` to check the latest available versions and install the specific version you need. ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Using Different Branches in Powerhouse](#using-different-branches-in-powerhouse) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
Uninstalling 'ph-cmd' ### How to uninstall Powerhouse CLI --- ### Problem statement You want to perform a clean installation of the Powerhouse CLI. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A terminal or IDE ### Solution ### Step 1: Uninstall `ph-cmd` ```bash pnpm uninstall -g ph-cmd ``` ### Step 2: Remove the global setups folder ```bash rm -rf ~/.ph ``` ### Expected outcome - Your system should now be clean from the Powerhouse CLI ### Common issues and solutions - Issue: Outdated version - Solution: Uninstall and reinstall the Powerhouse CLI ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Setting up or Resetting the Global Powerhouse Configuration](#setting-up-or-resetting-the-global-powerhouse-configuration) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools) - [Create A New Powerhouse Project](/academy/GetStarted/CreateNewPowerhouseProject)
Setting up or Resetting the Global Powerhouse Configuration ### How to set up or reset the global Powerhouse configuration --- ### Problem statement You need to initialize the global Powerhouse configuration for the first time, or reset it to resolve issues or start fresh. This might also involve switching to a specific dependency environment like staging. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - Terminal or command prompt access ### Solution ### Step 1: (Optional) Remove existing configuration If you suspect issues with your current global setup or want a completely clean slate, remove the existing global configuration directory. **Skip this if setting up for the first time.** ```bash # Use with caution: this removes your global settings and downloaded dependencies. rm -rf ~/.ph ``` ### Step 2: Set up global defaults Initialize the default global project configuration. ```bash ph setup-globals ``` ### Step 3: (Optional) Switch to a specific environment (e.g., staging) If you need to use non-production dependencies, switch the global environment. ```bash # Switch to staging dependencies ph use staging # Or switch back to the latest stable versions # ph use latest ``` ### Expected outcome - A `~/.ph` directory is created or reset. - The global project is configured, potentially using the specified environment (e.g., staging). - You are ready to initialize or work with Powerhouse projects using the defined global settings. ### Common issues and solutions - Issue: Commands fail after removing `~/.ph`. - Solution: Ensure you run `ph setup-globals` afterwards. - Issue: Need to use specific local dependencies globally. - Solution: Use `ph use local /path/to/local/packages`. ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Using Different Branches in Powerhouse](#using-different-branches-in-powerhouse) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools) - [GraphQL Schema Best Practices](/academy/MasteryTrack/WorkWithData/UsingTheAPI)
Using Different Branches in Powerhouse ### How to use different branches in Powerhouse --- ### Problem statement You need to access experimental features, bugfixes, or development versions of Powerhouse components that aren't yet available in the stable release. ### Prerequisites - Terminal or command prompt access - pnpm package manager 10 installed - Node.js 24 installed ### Solution ### Step 1: Install CLI with specific branch Choose the appropriate installation command based on your needs: ```bash # For latest stable version pnpm install -g ph-cmd # For development version pnpm install -g ph-cmd@dev # For staging version pnpm install -g ph-cmd@staging ``` ### Step 2: Initialize project with specific branch When creating a new project, you can specify which branch to use: ```bash # Use latest stable version of the boilerplate ph init # Use development version of the boilerplate ph init --dev # Use staging version of the boilerplate ph init --staging ``` ### Step 3: Switch dependencies for existing project For existing projects, you can switch all dependencies to different versions: ```bash # Switch to latest production versions ph use # Switch to development versions ph use dev # Switch to production versions ph use prod ``` ### Expected outcome - Access to the specified version of Powerhouse components - Ability to test experimental features or bugfixes - Project configured with the chosen branch's dependencies ### Common issues and solutions - Issue: Experimental features not working as expected - Solution: This is normal as these versions may contain untested features. Consider switching back to stable versions if issues persist. - Issue: Version conflicts between components - Solution: Ensure all components are using the same branch version. Use `ph use` commands to synchronize versions. ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Managing and Updating Powerhouse Dependencies](#managing-and-updating-powerhouse-dependencies) - [Setting up or Resetting the Global Powerhouse Configuration](#setting-up-or-resetting-the-global-powerhouse-configuration) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
Using Different Package Managers with Powerhouse ### How to use different package managers with Powerhouse --- ### Problem statement You want to use a different package manager (npm, yarn, or bun) instead of pnpm for managing Powerhouse projects and dependencies. ### Prerequisites - Node.js 24 installed - Your preferred package manager installed (npm, yarn, or bun) - Terminal or command prompt access ### Solution ### Step 1: Install the CLI with Your Preferred Package Manager Choose the appropriate installation command based on your package manager: ```bash # Using npm npm install -g ph-cmd --legacy-peer-deps # Using yarn yarn global add ph-cmd # Using bun bun install -g ph-cmd # Using pnpm (default) pnpm install -g ph-cmd ``` ### Step 2: Configure PATH for Global Binaries For yarn and bun, you need to add their global binary directories to your PATH: #### For Yarn: ```bash # Add this to your ~/.bashrc, ~/.zshrc, or equivalent export PATH="$PATH:$(yarn global bin)" ``` #### For Bun: ```bash # Add this to your ~/.bashrc, ~/.zshrc, or equivalent export PATH="$PATH:$HOME/.bun/bin" ``` After adding these lines, reload your shell configuration: ```bash source ~/.bashrc # or source ~/.zshrc ``` ### Step 3: Verify Installation Check that the CLI is properly installed and accessible: ```bash ph-cmd --version ``` ### Step 4: Using Different Package Managers in Projects When working with Powerhouse projects, you can specify your preferred package manager: ```bash # Initialize a project with npm ph init --package-manager npm # Initialize a project with yarn ph init --package-manager yarn # Initialize a project with bun ph init --package-manager bun # Initialize a project with pnpm (preferred default) ph init --package-manager pnpm ``` ### Expected outcome - Powerhouse CLI installed and accessible through your preferred package manager - Ability to manage Powerhouse projects using your chosen package manager - Proper PATH configuration for global binaries ### Common issues and solutions - Issue: Command not found after installation - Solution: Ensure the global binary directory is in your PATH (especially for yarn and bun) - Solution: Try running the command with the full path to verify installation - Issue: Permission errors during installation - Solution: Use `sudo` on Unix-based systems or run as administrator on Windows - Issue: Package manager conflicts - Solution: Stick to one package manager per project to avoid lockfile conflicts ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Setting up or Resetting the Global Powerhouse Configuration](#setting-up-or-resetting-the-global-powerhouse-configuration) ### Further reading - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools) - [Yarn Global Installation Guide](https://classic.yarnpkg.com/lang/en/docs/cli/global/) - [Bun Installation Guide](https://bun.sh/docs/installation#how-to-add-your-path)
## Powerhouse Package Development recipes This comprehensive section covers the complete workflow for building Powerhouse packages using Vetra Studio, from project initialization and document model creation to editors, Drive-apps, and package publishing. > **Tip:** For the best development experience, use **Vetra Studio** with `ph vetra --watch`. Vetra Studio provides automatic code generation, AI-assisted development, and live preview of your documents and editors. ### Vetra Studio Vetra Studio is the AI-powered development environment for building Powerhouse packages with specification-driven workflows.
Launching Vetra Studio ### How to Launch Vetra Studio --- ### Problem statement You want to start Vetra Studio to develop document models, editors, and other package components using the specification-driven workflow. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - Terminal or command prompt access ### Solution ### Step 1: Navigate to Your Project Directory ```bash cd ``` ### Step 2: Choose Your Launch Mode Vetra Studio offers three modes depending on your workflow: #### Interactive Mode (Recommended for Development) ```bash ph vetra --interactive ``` In interactive mode: - You receive confirmation prompts before any code generation - Changes require explicit confirmation before being processed - Provides better control and visibility over document changes #### Watch Mode with Interactive ```bash ph vetra --interactive --watch ``` In watch mode: - Enables dynamic loading for document-models and editors - The system watches for changes and reloads them dynamically - Best for active development with frequent changes #### Standard Mode ```bash ph vetra ``` In standard mode: - Changes are processed automatically with 1-second debounce - Multiple changes are batched and processed together - Uses the latest document state for processing ### Expected outcome - Vetra Studio launches in your browser - You can access the Vetra Studio Drive to manage specifications - Document models and editors are available for development ### Common issues and solutions - **Issue**: Vetra Studio environment breaks during document model development - **Solution**: This can happen when code changes break the environment. Restart Vetra Studio and check your document model for errors. - **Issue**: Changes not reflecting in the studio - **Solution**: If not using `--watch` mode, restart Vetra Studio to pick up changes. ### Related recipes - [Connecting to a Remote Vetra Drive](#connecting-to-a-remote-vetra-drive) - [Connecting Claude with Reactor MCP](#connecting-claude-with-reactor-mcp) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Powerhouse CLI Reference](/academy/APIReferences/PowerhouseCLI#vetra)
Connecting to a Remote Vetra Drive ### How to Connect to a Remote Vetra Drive --- ### Problem statement You want to collaborate with team members by connecting to a shared remote Vetra drive instead of using a local one, enabling synchronized specifications across your team. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized or ready to create - The URL of the remote Vetra drive you want to connect to - Terminal or command prompt access ### Solution #### Option A: Create a New Project with Remote Drive ### Step 1: Initialize with Remote Drive ```bash ph init --remote-drive ``` Example: ```bash ph init --remote-drive https://switchboard.staging.vetra.io/d/my-team-drive ``` ### Step 2: Start Vetra with Watch Mode ```bash ph vetra --watch ``` #### Option B: Clone an Existing Project from Remote Drive ### Step 1: Checkout the Remote Drive ```bash ph checkout --remote-drive ``` ### Step 2: Start Vetra ```bash ph vetra --watch ``` #### Option C: Configure Remote Drive in Existing Project ### Step 1: Edit powerhouse.config.json Add the Vetra configuration to your `powerhouse.config.json` file: ```json { "vetra": { "driveId": "your-drive-id", "driveUrl": "https://switchboard.staging.vetra.io/d/your-drive-id" } } ``` ### Step 2: Start Vetra ```bash ph vetra --watch ``` ### Expected outcome - Your project is connected to the remote Vetra drive - Specifications sync across team members - A "Vetra Preview" drive is created locally for testing changes before syncing - The main "Vetra" drive syncs with the remote and contains stable package configuration ### Common issues and solutions - **Issue**: Cannot connect to remote drive - **Solution**: Verify the drive URL is correct and accessible. Check your network connection and any firewall settings. - **Issue**: Local changes not syncing - **Solution**: The "Vetra Preview" drive is for local testing. Changes need to be explicitly synced to the main drive. - **Issue**: Conflicts with team member changes - **Solution**: Always use `ph vetra --watch` when restarting to ensure local documents and editors are loaded properly. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Connecting Claude with Reactor MCP](#connecting-claude-with-reactor-mcp) ### Further reading - [Vetra Remote Drive Reference](/academy/APIReferences/VetraRemoteDrive) - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio)
Connecting Claude with Reactor MCP ### How to Connect Claude with Reactor MCP --- ### Problem statement You want to use AI-assisted development in Vetra Studio by connecting Claude to the Reactor MCP (Model Context Protocol), enabling natural language document model creation and editing. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - Vetra Studio running (`ph vetra --watch`) - Claude CLI installed and configured - Terminal or command prompt access ### Solution ### Step 1: Start Vetra Studio First, ensure Vetra Studio is running in your project directory: ```bash ph vetra --interactive --watch ``` ### Step 2: Configure the MCP Server (one-time setup) Add the Reactor MCP server to your Claude configuration. For Claude Code, add it to `~/.claude/mcp.json`; for Claude Desktop, add it under MCP Servers in settings: ```json { "mcpServers": { "reactor-mcp": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:4001/mcp"] } } } ``` This tells Claude how to reach the Reactor MCP endpoint. You only need to do this once per machine. ### Step 3: Open a New Terminal and Navigate to Your Project ```bash cd ``` ### Step 4: Start Claude CLI ```bash claude ``` ### Step 5: Connect to Reactor MCP In the Claude CLI, request connection to the reactor: ``` connect to the reactor mcp ``` ### Step 6: Verify the Connection You should see a confirmation message like: ``` Connected to MCP successfully! I can see there's a "vetra-4de7fa45" drive available. The reactor-mcp server is running and ready for document model operations. ``` ### Expected outcome - Claude is connected to Reactor MCP - Vetra Studio shows "Connected to Reactor MCP" - You can now use natural language to create and modify document models - Claude has access to document operations, drive operations, and document model operations ### Common issues and solutions - **Issue**: MCP connection fails - **Solution**: Ensure Vetra Studio is running before attempting to connect Claude. Restart both Vetra and Claude if needed. - **Issue**: Claude doesn't recognize reactor commands - **Solution**: Make sure you're in the correct project directory when starting Claude. - **Issue**: Drive not visible in Claude - **Solution**: Verify Vetra Studio is running with the `--watch` flag and the drive is properly initialized. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Creating a Document Model with AI Assistance](#creating-a-document-model-with-ai-assistance) - [Creating an Editor with AI Assistance](#creating-an-editor-with-ai-assistance) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio)
Creating a Document Model with AI Assistance ### How to Create a Document Model with AI Assistance --- ### Problem statement You want to create a new document model using natural language descriptions through Claude and the Reactor MCP, rather than manually defining schemas and operations. ### Prerequisites - Vetra Studio running (`ph vetra --interactive --watch`) - Claude connected to Reactor MCP (see [Connecting Claude with Reactor MCP](#connecting-claude-with-reactor-mcp)) ### Solution ### Step 1: Describe Your Document Model to Claude Provide a detailed description of your document needs. Be specific about: - The purpose of the document - The data fields and their types - The operations users should be able to perform - Any relationships or constraints Example prompt: ``` Create a document model for a task tracker with the following requirements: - Each task has a title (string), description (string), status (enum: todo, in-progress, done), priority (enum: low, medium, high), and due date (optional date) - Users should be able to create tasks, update task details, change status, and delete tasks - Tasks should track when they were created and last modified ``` ### Step 2: Review the Generated Schema Claude will generate: - An appropriate GraphQL schema - The necessary operations - Implementation for the required reducers Review the proposed schema before confirming. ### Step 3: Confirm Generation in Interactive Mode If running in interactive mode, you'll be prompted to confirm: - Schema changes - Operation definitions - Code generation ### Step 4: Verify in Vetra Studio Check Vetra Studio to see your new document model in the drive. The document should appear with the defined schema and operations. ### Expected outcome - A new document model is created based on your natural language description - The schema, operations, and reducers are generated automatically - The document model is placed in the Vetra drive - Code scaffolding is generated in your project ### Common issues and solutions - **Issue**: Generated schema doesn't match expectations - **Solution**: Provide more specific requirements. Ask Claude clarifying questions before generation. - **Issue**: Operations missing functionality - **Solution**: Be explicit about all the actions users should be able to perform on the document. - **Issue**: Code generation fails - **Solution**: Check if the document model is in a valid state. Review any error messages in Vetra Studio. ### Related recipes - [Connecting Claude with Reactor MCP](#connecting-claude-with-reactor-mcp) - [Creating an Editor with AI Assistance](#creating-an-editor-with-ai-assistance) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Document Model Creation](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema)
Creating an Editor with AI Assistance ### How to Create an Editor with AI Assistance --- ### Problem statement You have a document model and want to create a user interface (editor) for it using AI assistance through Claude and the Reactor MCP. ### Prerequisites - Vetra Studio running (`ph vetra --interactive --watch`) - Claude connected to Reactor MCP - An existing document model in your Vetra drive ### Solution ### Step 1: Describe Your Editor Requirements to Claude Provide a detailed description including: - The document model the editor is for - UI layout and components needed - User interactions and workflows - Any specific styling or design requirements - Reference to operations from the document model Example prompt: ``` Create an editor for my task tracker document model with: - A form to create new tasks with fields for title, description, priority, and due date - A list view showing all tasks grouped by status (todo, in-progress, done) - Each task card should show title, priority badge, and due date - Clicking a task opens a detail panel for editing - Status can be changed via drag-and-drop between columns or a dropdown - Use the createTask, updateTask, changeStatus, and deleteTask operations ``` ### Step 2: Review Generated Components Claude will generate: - Editor components - Necessary hooks for document operations - Required UI elements - Integration with the document model operations ### Step 3: Confirm Generation In interactive mode, confirm the proposed changes before they are applied. ### Step 4: Verify in Your Project Check the `editors/` directory in your project for the generated editor files. The editor should be registered in your `powerhouse.manifest.json`. ### Step 5: Test the Editor Run Vetra Studio and open your document to test the new editor interface. ### Expected outcome - Editor components are generated in the `editors/` directory - The editor is registered in `powerhouse.manifest.json` - The editor integrates with your document model operations - You can interact with documents through the new UI ### Common issues and solutions - **Issue**: Editor doesn't appear in Vetra Studio - **Solution**: Verify the editor is registered in `powerhouse.manifest.json`. Restart Vetra Studio with `--watch`. - **Issue**: Operations not working in the editor - **Solution**: Ensure the editor references the correct operation names from your document model. - **Issue**: Styling doesn't match expectations - **Solution**: Provide more detailed design requirements or manually adjust the generated CSS/styles. ### Related recipes - [Creating a Document Model with AI Assistance](#creating-a-document-model-with-ai-assistance) - [Generating a Document Editor](#generating-a-document-editor) - [Connecting Claude with Reactor MCP](#connecting-claude-with-reactor-mcp) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Build a Todo-list Editor](/academy/GetStarted/BuildToDoListEditor)
### Project Initialization & Management Creating, configuring, and managing Powerhouse projects, which are collections of document models, editors, and other resources.
Initializing a New Project & Document Model ### How to initialize a new project and document model --- ### Problem statement You need to create a new, empty document model within a Powerhouse project to represent a workflow of a business process. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (see [Initializing a Powerhouse Project Recipe](#powerhouse-cli-recipes)) or follow Step 1 & 2 below. - Access to a terminal or command prompt - A web browser ### Solution > **Recommended:** Use **Vetra Studio** for document model development. Vetra provides automatic code generation and a live preview with `ph vetra --watch`. See [Launching Vetra Studio](#launching-vetra-studio) for the preferred workflow. ### Step 1: Initialize a Powerhouse Project (if needed) If you haven't already, create a new Powerhouse project: ```bash ph init # Follow the prompts to name your project ``` ### Step 2: Navigate to Project Directory Change your current directory to the newly created project folder: ```bash cd ``` ### Step 3: Start Vetra Studio (Recommended) Run Vetra with watch mode for automatic code generation and live preview: ```bash ph vetra --watch ``` This will: - Launch Vetra Studio in your browser - Automatically generate code when you make changes to document models - Provide live preview of your documents and editors ### Step 4: Create the Document Model In Vetra Studio, navigate to your drive and click the `DocumentModel` button to create a new document model. ### Alternative: Using Connect (Legacy) If you need to use the Connect application instead: ```bash ph connect ``` Wait for the output indicating the server is running (e.g., `Local: http://localhost:3000/`). ### Expected outcome - An empty document model is created and opened in the Document Model Editor. - You are ready to start defining the schema and logic for your new model. - With Vetra, code is automatically generated as you make changes. ### Common issues and solutions - Issue: `ph vetra` command fails. - Solution: Ensure `ph-cmd` is installed correctly (`ph-cmd --version`). Check for port conflicts. Make sure you are inside the project directory created by `ph init`. - Issue: Browser window doesn't open automatically. - Solution: Manually open the URL shown in the terminal output. - Issue: Cannot find the `DocumentModel` button. - Solution: Ensure you have navigated into your drive within the application first. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Creating a Document Model with AI Assistance](#creating-a-document-model-with-ai-assistance) - [Initializing a Powerhouse Project](#powerhouse-cli-recipes) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [GraphQL Schema Best Practices](/academy/MasteryTrack/WorkWithData/UsingTheAPI)
Generating Reducers from a Document Model File ### How to Generate Reducers from a Document Model File --- ### Problem statement You have a Powerhouse Document Model defined in a `.phdm` or `.phdm.zip` file and need to generate the corresponding reducer functions for your project. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - A `.phdm` or `.phdm.zip` file containing your document model definition, placed in your project (e.g., at the root). ### Solution > **Recommended:** Use **Vetra Studio** with `ph vetra --watch` for automatic code generation. Vetra watches for changes to your document models and automatically generates reducers and other code. See [Launching Vetra Studio](#launching-vetra-studio). ### Using Vetra (Recommended) With Vetra running in watch mode, code generation happens automatically: ```bash ph vetra --watch ``` When you make changes to document models in Vetra Studio, reducers and other code are generated automatically. ### Manual Generation (Alternative) If you need to manually generate code from a `.phdm` file: ### Step 1: Navigate to Project Directory Ensure your terminal is in the root directory of your Powerhouse project. ```bash cd ``` ### Step 2: Run the Generate Command Execute the `ph generate` command, providing the path to your document model file. ```bash # Replace todo.phdm.zip with the actual filename/path of your model ph generate todo.phdm.zip ``` ### Step 3: Integrate Generated Code The command will output the generated reducer scaffolding code in the designated folders. ### Expected outcome - Reducer functions corresponding to the operations defined in your document model are generated. - The generated code is ready to be integrated into your project's state management logic. - With Vetra, this happens automatically when you save changes. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model) - [Generating a Document Editor](#generating-a-document-editor)
Managing and Updating Powerhouse Dependencies ### How to Manage and Update Powerhouse Dependencies --- ### Problem statement You need to understand and manage different types of dependencies in your Powerhouse project, and know how to update them using `ph update` and `ph use` commands. This includes updating based on version ranges, switching between development environments, and understanding the Powerhouse branching strategy. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - Terminal or command prompt access ### Solution ### Understanding Dependency Types #### 1. Monorepo Dependencies (Powerhouse Core) The Powerhouse monorepo uses a specific branching strategy: - **Development** (`dev` tag): Ongoing development on the main branch - **Staging** (`staging` tag): Pre-release branch (`Release/staging/v.x.x`) - **Production** (`latest` or `prod` tag): Latest stable release (`Release/production/v.x.x`) You can install CLI versions matching these environments: ```bash # Install dev version of CLI pnpm install -g ph-cmd@dev # Install staging version pnpm install -g ph-cmd@staging # Install latest stable version pnpm install -g ph-cmd ``` #### 2. Project Dependencies These are dependencies from published npm packages in your `package.json`. ### Using `ph update`: Version Range Updates Use `ph update` to update dependencies based on the semver ranges in your `package.json`: #### Step 1: Standard Update (Respect Version Ranges) Update all Powerhouse dependencies within the ranges specified in your `package.json`: ```bash ph update ``` This updates packages like `@powerhousedao/*` and `document-model` to their latest versions that satisfy your version constraints. ### Step 2: Force Update to Specific Environment Override version ranges and force all dependencies to a specific environment: ```bash # Force update to latest dev versions ph update --force dev # Force update to latest stable/production versions ph update --force prod # or ph update --force latest ``` #### Step 3: Specify Package Manager (Optional) ```bash ph update --package-manager pnpm ``` ### Using `ph use`: Environment Switching Use `ph use` to quickly switch all dependencies between environments or versions: #### Switching to Development Environment ```bash ph use dev ``` This switches all installed Powerhouse dependencies to their `@dev` tagged versions. #### Switching to Staging Environment ```bash ph use staging ``` #### Switching to Production Environment ```bash ph use prod # or ph use latest ``` #### Switching to Specific Version ```bash # Use a specific release version ph use 5.1.0 # Use a pre-release version ph use 1.0.0-beta.1 ``` #### Using Local Development Versions For active development, link to local packages: ```bash ph use local /path/to/powerhouse/monorepo ``` #### Resolving Tags to Exact Versions Use `--use-resolved` to resolve tags to actual version numbers: ```bash ph use dev --use-resolved ``` This resolves `@dev` to an exact version like `v1.0.1-dev.1` instead of using the tag. ### Initializing Projects with Specific Environments When creating new projects, you can specify the environment: ```bash # Initialize with dev dependencies ph init my-project --dev # Initialize with staging dependencies ph init my-project --staging # Initialize with production dependencies (default) ph init my-project ``` ### Publishing Updated Dependencies If you're developing packages: 1. Update dependencies in your project using `ph use` or `ph update` 2. Test thoroughly 3. Publish the updated package to npm: ```bash pnpm build npm publish ``` 4. Other projects get the new version when they run `ph install your-package` ### Expected outcome - Clear understanding of Powerhouse dependency types and environments - Ability to update dependencies based on version ranges with `ph update` - Ability to switch between environments with `ph use` - Knowledge of the dev/staging/prod branching strategy - Understanding of when to use each command ### Common issues and solutions - **Issue**: Dependencies not updating as expected - **Solution**: Check your `package.json` version ranges. Use `ph update --force ` to override ranges. - **Issue**: Confusion between `ph update` and `ph use` - **Solution**: Use `ph update` for version range updates. Use `ph use` for environment switching. - **Issue**: Breaking changes after updates - **Solution**: Test thoroughly. Consider publishing to a private npm registry first. Use staging environment before production. - **Issue**: Local development changes not reflecting - **Solution**: Use `ph use local /path/to/monorepo` and ensure you've built the local packages. - **Issue**: Which version am I using? - **Solution**: Check `package.json` for installed versions. Use `ph list` to see installed packages. ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Using Different Branches in Powerhouse](#using-different-branches-in-powerhouse) - [Setting up or Resetting the Global Powerhouse Configuration](#setting-up-or-resetting-the-global-powerhouse-configuration) - [Installing a Custom Powerhouse Package](#installing-a-custom-powerhouse-package) ### Further reading - [Powerhouse CLI Reference: Update Command](/academy/APIReferences/PowerhouseCLI#update) - [Powerhouse CLI Reference: Use Command](/academy/APIReferences/PowerhouseCLI#use) - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
Running Connect with HTTPS and a Custom Port ### How to Run Connect with HTTPS and a Custom Port --- ### Problem statement You need to run the local Powerhouse application using HTTPS, possibly on a different port than the default, for scenarios like testing on a remote server (e.g., EC2) or complying with specific network requirements. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - Potentially, valid SSL/TLS certificates if running in a non-localhost environment that requires trusted HTTPS. (The `--https` flag may use self-signed certificates for local development). ### Solution > **Note:** For local development, **Vetra Studio** (`ph vetra --watch`) is the recommended workflow as it provides automatic code generation and live preview. Use the options below when you specifically need HTTPS or custom port configurations. ### Step 1: Navigate to Project Directory Ensure your terminal is in the root directory of your Powerhouse project. ```bash cd ``` ### Step 2: Run with Flags #### Using Vetra (Recommended for Development) ```bash # Vetra with watch mode for automatic code generation ph vetra --watch ``` #### Using Connect (for HTTPS/Custom Port) Execute the `ph connect` command, adding the `--https` flag to enable HTTPS and the `--port` flag followed by the desired port number. ```bash # Example using port 8442 ph connect --port 8442 --https ``` ### Step 3: Access the Application Open your web browser and navigate to the specified address. Remember to use `https` and include the custom port. ``` https://: # Example: https://localhost:8442 # Example: https://my-ec2-instance-ip:8442 ``` You might encounter a browser warning about the self-signed certificate; you may need to accept the risk to proceed for local/development testing. ### Expected outcome - The Powerhouse application starts and serves traffic over HTTPS on the specified port. - You can access the interface securely using the `https` protocol. ### Common issues and solutions - Issue: Browser shows security warnings (e.g., "Your connection is not private"). - Solution: This is expected when using the default self-signed certificate generated by `--https`. For development or internal testing, you can usually proceed by accepting the risk. For production or public-facing scenarios, configure with properly signed certificates (consult Powerhouse documentation for advanced configuration). - Issue: Port conflict (e.g., `"Port is already in use"`). - Solution: Choose a different port number that is not currently occupied by another application. - Issue: Cannot access from a remote machine. - Solution: Ensure the port is open in any firewalls (on the server and potentially network firewalls). Verify you are using the correct public IP address or hostname of the machine. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
### Editors & Drive-apps Generating and customizing editors for Document Models and custom interfaces for Drives.
Generating a Document Editor ### How to Generate a Document Editor --- ### Problem statement You have a Powerhouse document model and need to create a user interface (editor) for it. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - A document model generated or defined within the project (e.g., in the `document-models` directory). ### Solution > **Recommended:** Use **Vetra Studio** with `ph vetra --watch` for editor development. Vetra automatically generates editor scaffolding and provides live preview as you develop. See [Launching Vetra Studio](#launching-vetra-studio) and [Creating an Editor with AI Assistance](#creating-an-editor-with-ai-assistance). ### Using Vetra (Recommended) Start Vetra with watch mode for automatic code generation and live preview: ```bash ph vetra --watch ``` In Vetra Studio, you can: - Create editors visually or with AI assistance - See live preview of your editor as you make changes - Automatically generate editor scaffolding ### Manual Generation (Alternative) If you need to manually generate an editor template: ### Step 1: Navigate to Project Directory Ensure your terminal is in the root directory of your Powerhouse project. ```bash cd ``` ### Step 2: Generate the Editor Template Run the `generate` command, specifying the editor name (usually matching the document model name) and the associated document type. ```bash # Replace with the name of your document model (e.g., To-do List) # Replace with the identifier for your document (e.g., powerhouse/todo-list) ph generate --editor --document-types ``` ### Expected outcome - A new directory is created under `editors/` (e.g., `editors//`). - An `editor.tsx` file is generated within that directory, containing a basic template for your document editor. - You can now customize `editor.tsx` to build your desired UI using HTML, Tailwind CSS, or custom CSS. - With Vetra, you get live preview with `ph vetra --watch` as you develop. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Creating an Editor with AI Assistance](#creating-an-editor-with-ai-assistance) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model) - [Generating a Custom Drive-app](#generating-a-custom-drive-app) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Build a Todo-list Editor](/academy/GetStarted/BuildToDoListEditor)
Generating a Custom Drive-app ### How to Generate a Custom Drive-app --- ### Problem statement You need a custom, application-like interface to browse, organize, or interact with specific types of documents stored within a Powerhouse drive, going beyond the standard file listing. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) ### Solution > **Recommended:** Use **Vetra Studio** with `ph vetra --watch` for drive explorer development. Vetra provides automatic code generation and live preview as you build your custom drive interface. See [Launching Vetra Studio](#launching-vetra-studio). ### Using Vetra (Recommended) Start Vetra with watch mode for automatic code generation and live preview: ```bash ph vetra --watch ``` Vetra Studio allows you to develop and preview your Drive-app in real-time. ### Manual Generation (Alternative) If you need to manually generate a Drive-app template: ### Step 1: Navigate to Project Directory Ensure your terminal is in the root directory of your Powerhouse project. ```bash cd ``` ### Step 2: Generate the Drive-app Template Run the `generate` command, specifying the `--drive-editor` flag and a name for your Drive-app. ```bash # Replace with a suitable name for your Drive-app (e.g., todo-drive-app) ph generate --drive-editor ``` ### Expected outcome - A new directory is created under `editors/` (e.g., `editors//`). - Template files (`EditorContainer.tsx`, components, hooks, etc.) are generated within that directory, providing a basic structure for a Drive-app. - You can now customize these files to create your specific drive interface, potentially removing default components and adding custom views relevant to your document models. - Remember to update your `powerhouse.manifest.json` to register the new app. - With Vetra, you get live preview with `ph vetra --watch` as you develop. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Generating a Document Editor](#generating-a-document-editor) ### Further reading - [Vetra Studio Documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio) - [Build a Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer)
Adding a New Drive via GraphQL Mutation ### How to Add a New Remote Drive via GraphQL Mutation --- ### Problem statement You want to programmatically add a new remote drive to your Powerhouse Connect environment using a GraphQL mutation. This is useful for automation, scripting, or integrating with external systems. ### Prerequisites - Access to the Switchboard or remote reactor (server node) of your Connect instance. - The GraphQL endpoint for your instance (e.g., `https://staging.switchboard.phd/graphql/system`). - Appropriate permissions to perform mutations. ### Solution ### Step 1: Access the GraphQL Playground or Client Open the GraphQL Playground at your endpoint (e.g., [https://staging.switchboard.phd/graphql/system](https://staging.switchboard.phd/graphql/system)), or use a GraphQL client of your choice. ### Step 2: Prepare the Mutation Use the following mutation to create a new drive, set a name and add a drive icon. Weither or not you define a ID & Slug is up to you: ```graphql mutation Mutation( $name: String! $icon: String $addDriveId: String $slug: String ) { addDrive(name: $name, icon: $icon, id: $addDriveId, slug: $slug) { icon id name slug } } ``` Example variables: ```json { "name": "AcademyTest", "icon": "https://static.thenounproject.com/png/3009860-200.png", "addDriveId": null, "slug": null } ``` You can also provide a custom `id`, `slug`, or `preferredEditor` if needed. ### Step 3: Execute the Mutation Run the mutation. On success, you will receive a response containing the new drive's `icon`, `id`, `name`, and `slug`: ```json { "data": { "addDrive": { "icon": "https://static.thenounproject.com/png/3009860-200.png", "id": "6461580b-d317-4596-942d-f6b3d1bfc8fd", "name": "AcademyTest", "slug": "6461580b-d317-4596-942d-f6b3d1bfc8fd" } } } ``` ### Step 4: Construct the Drive URL Once you have the `id` or `slug`, you can construct the drive URL for Connect: - Format: `domain/d/driveId` or `domain/d/driveSlug` - Example: `https://staging.connect.phd/d/6461580b-d317-4596-942d-f6b3d1bfc8fd` ### Step 5: Add the Drive in Connect Use the constructed URL to add or access the drive in your Connect environment. ### Expected outcome - A new drive is created and accessible in your Connect environment. - The drive can be managed or accessed using the generated URL. ### Related recipes - [Configuring Drives](/academy/MasteryTrack/WorkWithData/ConfiguringDrives) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model) ### Further reading - [GraphQL Playground](https://www.apollographql.com/docs/apollo-server/testing/graphql-playground/) - [Powerhouse Builder Tools](/academy/MasteryTrack/BuilderEnvironment/BuilderTools)
### Package Publishing & Distribution Creating, installing, and managing Powerhouse Packages for distribution and reuse. ## Deployment recipes This section covers deploying Powerhouse applications and packages to various environments using Docker and other deployment methods. ## Reactor & Data Synchronisation recipes This section covers managing the Powerhouse Reactor (the local service for processing document model operations) and troubleshooting data synchronization within the Powerhouse ecosystem. > **Tip:** For development workflows, **Vetra Studio** (`ph vetra --watch`) is recommended as it includes reactor functionality along with automatic code generation and live preview. ### Reactor Recipes The [Powerhouse Recipes](https://github.com/powerhouse-inc/recipes) repository contains standalone example projects that demonstrate common Reactor patterns and integrations. Each recipe is a self-contained project you can clone and run. | Recipe | Description | | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [Audit Trail](https://github.com/powerhouse-inc/recipes/tree/main/audit-trail) | Reactor processor that builds an immutable audit log in PostgreSQL from `ActionSigner` context, with a GraphQL subgraph for querying entries by user, document, or time range. | | [Batch Progress](https://github.com/powerhouse-inc/recipes/tree/main/batch-progress) | Multi-document creation with dependency ordering using `executeBatch` and real-time progress tracking via the EventBus. | | [Cross-Document Reactor](https://github.com/powerhouse-inc/recipes/tree/main/cross-document-reactor) | Event-driven cross-document automation using `ReactorClient` subscriptions to dispatch actions across related documents. | | [Custom Read Model](https://github.com/powerhouse-inc/recipes/tree/main/custom-read-model) | Custom `IReadModel` registered via `ReactorBuilder.withReadModel()` that maintains a document-count-per-type materialized view. | | [Discord Webhook Processor](https://github.com/powerhouse-inc/recipes/tree/main/discord-webhook-processor) | Reactor processor that posts document operations to a Discord channel as rich embeds with automatic batching and HMAC-SHA256 signatures. | | [Document Snapshot Exporter](https://github.com/powerhouse-inc/recipes/tree/main/document-snapshot-exporter) | CLI tool for reliable read-after-write export of document state and operation history to JSON using Reactor consistency tokens. | | [Full-Text Search](https://github.com/powerhouse-inc/recipes/tree/main/full-text-search) | Reactor processor that indexes document state into a PostgreSQL full-text search table for ranked keyword search across all documents. | | [Rate Limiter](https://github.com/powerhouse-inc/recipes/tree/main/rate-limiter) | Reactor processor paired with an `AuthService` gate to throttle users by signer address using a sliding window. | | [Relational DB Subgraph](https://github.com/powerhouse-inc/recipes/tree/main/relational-db-subgraph) | Complete relational DB processor with Kysely migrations, typed schema, and a GraphQL subgraph for a document catalog. | | [Saga](https://github.com/powerhouse-inc/recipes/tree/main/saga) | Saga pattern via Reactor processor: operations on one document trigger operations on others, linked by a traceable saga context with re-entrancy guards. | | [Signed Operations Verifier](https://github.com/powerhouse-inc/recipes/tree/main/signed-operations-verifier) | Standalone script that builds document operations with cryptographic signatures and verifies each one, demonstrating tamper detection via `ISigner`. | | [Subscription CLI](https://github.com/powerhouse-inc/recipes/tree/main/subscription-cli) | CLI for monitoring Reactor GraphQL subscriptions over WebSocket in real time, printing timestamped events to stdout. | | [Sync Health Monitor](https://github.com/powerhouse-inc/recipes/tree/main/sync-health-monitor) | EventBus-based sync health dashboard with two-reactor sync monitoring and a GraphQL subgraph for metrics. | > See the [recipes repository](https://github.com/powerhouse-inc/recipes) for full source code, setup instructions, and prerequisites. ### Reactor Management
Starting the Reactor ### How to Start the Powerhouse Reactor --- ### Problem statement You need to start the Powerhouse Reactor, the local service responsible for processing document model operations and managing state, typically for testing or development purposes. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) - You are in the root directory of your Powerhouse project. ### Solution > **Note:** For development, **Vetra Studio** (`ph vetra --watch`) is the recommended workflow as it includes the reactor functionality along with automatic code generation and live preview. Use `ph reactor` directly when you need to run the reactor service independently. ### Using Vetra (Recommended for Development) ```bash ph vetra --watch ``` Vetra includes reactor functionality and provides: - Automatic code generation when document models change - Live preview of documents and editors - Integrated development environment ### Using Reactor Directly ### Step 1: Navigate to Project Directory (if needed) Ensure your terminal is in the root directory of your Powerhouse project. ```bash cd ``` ### Step 2: Run the Reactor Command Execute the `ph reactor` command. ```bash ph reactor ``` ### Expected outcome - The Reactor service starts, typically listening on `localhost:4001`. - You will see log output indicating the reactor is running and ready to process operations. - A GraphQL endpoint is usually available at `http://localhost:4001/graphql` for direct interaction and testing. ### Common issues and solutions - Issue: Reactor fails to start, mentioning port conflicts. - Solution: Ensure port `4001` (or the configured reactor port) is not already in use by another application. Stop the conflicting application or configure the reactor to use a different port (if possible, check documentation). - Issue: Errors related to storage or configuration. - Solution: Check the `powerhouse.manifest.json` and any reactor-specific configuration files for errors. Ensure storage providers (like local disk) are accessible and configured correctly. ### Related recipes - [Launching Vetra Studio](#launching-vetra-studio) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model)
Deploying Powerhouse with Docker ### How to Deploy Powerhouse with Docker --- ### Problem statement You want to deploy your Powerhouse application (Connect and Switchboard) using Docker containers for production or development environments. Docker deployment provides consistency, reproducibility, and easy scalability across different platforms. ### Prerequisites - Docker installed on your system - Docker Compose installed (usually included with Docker Desktop) - Basic understanding of Docker concepts - (Optional) A custom Powerhouse package to deploy ### Solution ### Step 1: Create a Docker Compose Configuration Create a `docker-compose.yml` file in your project directory: ```yaml name: powerhouse services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:latest environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres - PH_CONNECT_BASE_PATH=/ ports: - "127.0.0.1:3000:4000" networks: - powerhouse_network depends_on: postgres: condition: service_healthy switchboard: image: ghcr.io/powerhouse-inc/powerhouse/switchboard:latest environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres ports: - "127.0.0.1:4000:4001" networks: - powerhouse_network depends_on: postgres: condition: service_healthy postgres: image: postgres:16.1 ports: - "127.0.0.1:5444:5432" environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres - POSTGRES_USER=postgres volumes: - postgres_data:/var/lib/postgresql/data networks: - powerhouse_network healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 3 networks: powerhouse_network: name: powerhouse_network volumes: postgres_data: ``` ### Step 2: Install Custom Packages (Optional) If you have custom Powerhouse packages to deploy, add them via the `PH_PACKAGES` environment variable: ```yaml services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:latest environment: - PH_PACKAGES=@powerhousedao/your-package,@powerhousedao/another-package - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres ``` ### Step 3: Start the Services Launch all services in detached mode: ```bash docker compose up -d ``` ### Step 4: Verify Deployment Check that all services are running: ```bash docker compose ps ``` View logs to confirm successful startup: ```bash docker compose logs -f ``` ### Step 5: Access Your Application Once services are running, access: - **Connect**: http://localhost:3000 - **Switchboard API**: http://localhost:4000 ### Production Configuration For production deployments, use specific version tags and secure credentials: ```yaml services: connect: image: ghcr.io/powerhouse-inc/powerhouse/connect:v1.0.0 env_file: - .env switchboard: image: ghcr.io/powerhouse-inc/powerhouse/switchboard:v1.0.0 env_file: - .env ``` Create a `.env` file with secure credentials: ```bash POSTGRES_PASSWORD=your-secure-password DATABASE_URL=postgres://powerhouse:your-secure-password@postgres:5432/powerhouse ``` ### Expected outcome - All Powerhouse services (Connect, Switchboard, PostgreSQL) are running in Docker containers - Services can communicate with each other through the Docker network - Your custom packages (if specified) are installed and available - The application is accessible through the configured ports - Data is persisted in Docker volumes ### Common issues and solutions - **Issue**: Container fails to start with "port already in use" error - **Solution**: Change the port mapping in `docker-compose.yml` to use an available port (e.g., `3001:4000` instead of `3000:4000`) - **Issue**: Database connection errors on startup - **Solution**: Ensure the `depends_on` configuration includes health checks so services wait for PostgreSQL to be ready - **Issue**: Custom packages fail to install - **Solution**: Verify package names are correct and published to npm. Check container logs with `docker compose logs switchboard` or `docker compose logs connect` - **Issue**: Changes to docker-compose.yml not taking effect - **Solution**: Run `docker compose down` then `docker compose up -d` to recreate containers with new configuration - **Issue**: Permission errors with volumes - **Solution**: Ensure the volume paths have correct permissions: `sudo chown -R 1000:1000 ./data` ### Related recipes - [Installing a Custom Powerhouse Package](#installing-a-custom-powerhouse-package) - [Setting up a Production Environment](#setting-up-a-production-environment) - [Publishing a Powerhouse Package](#packaging-and-publishing-a-powerhouse-project) ### Further reading - [Docker Deployment Guide](/academy/MasteryTrack/Launch/DockerDeployment) - [Environment Configuration](/academy/MasteryTrack/Launch/ConfigureEnvironment) - [Setup Environment Guide](/academy/MasteryTrack/Launch/SetupEnvironment) - [Docker Compose Documentation](https://docs.docker.com/compose/)
Setting up a Production Environment ### How to set up a Production Powerhouse Environment --- ### Problem statement You need to set up a new production-ready server to host and run your Powerhouse services (Connect and Switchboard). ### Prerequisites - A Linux-based server (Ubuntu or Debian recommended) with `sudo` privileges. - A registered domain name. - DNS `A` records for your `connect` and `switchboard` subdomains pointing to your server's public IP address. ### Solution ### Step 1: Install Powerhouse Services SSH into your server and run the universal installation script. This will install Node.js, pnpm, and prepare the system for Powerhouse services. ```bash curl -fsSL https://apps.powerhouse.io/install | bash ``` ### Step 2: Reload Your Shell After the installation, reload your shell's configuration to recognize the new commands. ```bash source ~/.bashrc # Or source ~/.zshrc if using zsh ``` ### Step 3: Initialize a Project Create a project directory for your services. The `ph-init` command sets up the basic structure. Move into the directory after creation. ```bash ph-init my-powerhouse-services cd my-powerhouse-services ``` ### Step 4: Configure Services Run the interactive setup command. This will guide you through configuring Nginx, PM2, databases, and SSL. ```bash ph service setup ``` During the setup, you will be prompted for: - **Packages to install:** You can pre-install any Powerhouse packages you need. (Optional) - **Database:** Choose between a local PostgreSQL setup or connecting to a remote database. - **SSL Certificate:** Select Let's Encrypt for a production setup. You will need to provide your domain and subdomains. ### Expected outcome - Powerhouse Connect and Switchboard services are installed, configured, and running on your server. - Nginx is set up as a reverse proxy with SSL certificates from Let's Encrypt. - Services are managed by PM2 and will restart automatically on boot or if they crash. - You can access your services securely at `https://connect.yourdomain.com` and `https://switchboard.yourdomain.com`. ### Common issues and solutions - **Issue:** `ph: command not found` - **Solution:** Ensure you have reloaded your shell with `source ~/.bashrc` or have restarted your terminal session. - **Issue:** Let's Encrypt SSL certificate creation fails. - **Solution:** Verify that your domain's DNS records have fully propagated and are pointing to the correct server IP. This can take some time. - **Issue:** Services fail to start. - **Solution:** Check the service logs for errors using `ph service logs` or `pm2 logs`. ### Related recipes - [Installing a Custom Powerhouse Package](#installing-a-custom-powerhouse-package) - [Deploying Powerhouse with Docker](#deploying-powerhouse-with-docker) ### Further reading - [Full Setup Guide](/academy/MasteryTrack/Launch/SetupEnvironment)
Installing a Custom Powerhouse Package ### How to Install a Custom Powerhouse Package --- ### Problem statement You have developed and published a Powerhouse package (containing document models, editors, etc.) to npm, or you have a local package, and you need to install it into another Powerhouse project. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed - A Powerhouse project initialized (`ph init`) where you want to install the package. - The custom package is either published to npm or available locally. ### Solution ### Step 1: Navigate to the Target Project Directory Ensure your terminal is in the root directory of the Powerhouse project where you want to install the package. ```bash cd ``` ### Step 2: Install the Package Use the `ph install` command followed by the package name (if published to npm) or the path to the local package. **For npm packages:** ```bash # Replace with the actual name on npm ph install ``` **For local packages (using a relative or absolute path):** ```bash # Example using a relative path ph install ../path/to/my-local-package # Example using an absolute path ph install /Users/you/dev/my-local-package ``` ### Step 3: Verify Installation Check your project's `package.json` and `powerhouse.manifest.json` to ensure the package dependency has been added correctly. Run `ph vetra --watch` (or `ph connect`) to see if the components from the installed package are available. ### Expected outcome - The custom Powerhouse package is downloaded and installed into your project's dependencies. - The `powerhouse.manifest.json` is updated (if necessary) to reflect the installed package. - Document models, editors, Drive-apps, or other components from the package become available within the target project. ### Common issues and solutions - Issue: Package not found (npm). - Solution: Double-check the package name for typos. Ensure the package is published and accessible on npm. - Issue: Path not found (local). - Solution: Verify the relative or absolute path to the local package directory is correct. - Issue: Conflicts with existing project components or dependencies. - Solution: Resolve version conflicts or naming collisions as needed. Review the installed package's structure and dependencies. ### Related recipes - [Publishing a Powerhouse Package](#publishing-a-powerhouse-package) - [Initializing a Powerhouse Project](#initializing-a-new-project-and-document-model)
Packaging and Publishing a Powerhouse Project ### How to Package and Publish a Powerhouse Project --- ### Problem statement You have created a collection of document models, editors, or other components and want to share it as a reusable package on a public or private npm registry. Publishing a package allows other projects to install and use your creations easily. ### Prerequisites - A completed Powerhouse project that you are ready to share. - An account on [npmjs.com](https://www.npmjs.com/) (or a private registry). - Your project's `package.json` should have a unique name and correct version. - You must be logged into your npm account via the command line. ### Solution ### Step 1: Build the Project First, compile your project to create a production-ready build in the `dist/` or `build/` directory. ```bash pnpm build ``` ### Step 2: Log In to npm If you aren't already, log in to your npm account. You will be prompted for your username, password, and one-time password. ```bash npm login ``` ### Step 3: Version Your Package Update the package version according to semantic versioning. This command updates `package.json` and creates a new Git tag. ```bash # Choose one depending on the significance of your changes pnpm version patch # For bug fixes (e.g., 1.0.0 -> 1.0.1) pnpm version minor # For new features (e.g., 1.0.1 -> 1.1.0) pnpm version major # For breaking changes (e.g., 1.1.0 -> 2.0.0) ``` ### Step 4: Publish the Package Publish your package to the npm registry. If it's your first time publishing a scoped package (e.g., `@your-org/your-package`), you may need to add the `--access public` flag. ```bash npm publish --access public ``` ### Step 5: Push Git Commits and Tags Push your new version commit and tag to your remote repository to keep it in sync. ```bash # Push your current branch git push # Push the newly created version tag git push --tags ``` ### Expected outcome - Your Powerhouse project is successfully published to the npm registry. - Other developers can now install your package into their projects using `ph install @your-org/your-package-name`. - Your Git repository is updated with the new version information. ### Common issues and solutions - **Issue**: "403 Forbidden" or "You do not have permission" error on publish. - **Solution**: Ensure your package name is unique and not already taken on npm. If it's a scoped package (`@scope/name`), make sure the organization exists and you have permission to publish to it. For public scoped packages, you must include `--access public`. ### Related recipes - [Installing a Custom Powerhouse Package](#installing-a-custom-powerhouse-package) - [Managing and Updating Powerhouse Dependencies](#managing-and-updating-powerhouse-dependencies)
### Data Synchronisation
Troubleshooting Document Syncing: Supergraph vs. Drive Endpoints ### Troubleshooting Document Syncing: Supergraph vs. Drive Endpoints --- ### Problem statement You've created or modified documents within a specific drive using Powerhouse Connect, but when you query the main GraphQL endpoint (`http://localhost:4001/graphql`), you don't see the changes or the documents you expected. This can lead to confusion about whether data is being synced correctly. ### Prerequisites - Powerhouse CLI (`ph-cmd`) installed. - A Powerhouse project initialized (`ph init`). - Vetra Studio is running (`ph vetra --watch`) or the Powerhouse Reactor is running (`ph reactor`). - You have attempted to create or modify documents in a drive (e.g., a "finances" drive). ### Solution Understanding the different GraphQL endpoints in Powerhouse is crucial for effective troubleshooting: 1. **The Supergraph Endpoint (`http://localhost:4001/graphql`):** - This is the main entry point for the supergraph, which combines various subgraphs (e.g., system information, user accounts, etc.). - While you can query many things here, it's generally **not** the endpoint for direct, real-time document content operations like `pushUpdates` for a specific drive. 2. **Drive-Specific Endpoints (e.g., `http://localhost:4001/d/` or `http://localhost:4001/d//graphql`):** - Each drive (e.g., "finances", "mydocs") has its own dedicated endpoint. - Operations that modify or directly interact with the content of a specific drive, such as creating new documents or pushing updates, are typically handled by this endpoint. - When you interact with documents in Powerhouse Connect, it communicates with these drive-specific endpoints. **Troubleshooting Steps:** 1. **Identify the Correct Endpoint:** - As illustrated in the scenario where a user was looking for documents in a "finances" drive, the key realization was needing to interact with the `http://localhost:4001/d/finances` endpoint for document-specific operations, not just `http://localhost:4001/graphql`. 2. **Inspect Network Requests:** - Open your browser's developer tools (usually by pressing F12) and go to the "Network" tab. - Perform an action in Powerhouse Connect that involves a document (e.g., creating, saving). - Look for GraphQL requests. You'll often see operations like `pushUpdates`. - Examine the "Request URL" or "Path" for these requests. You'll likely see they are being sent to a drive-specific endpoint (e.g., `/d/finances`, `/d/powerhouse`). - The payload might show `operationName: "pushUpdates"`, confirming a document modification attempt. 3. **Querying Drive Data:** - If you want to query the state of documents within a specific drive via GraphQL, ensure you are targeting that drive's GraphQL endpoint (often `http://localhost:4001/d//graphql` or through specific queries available on the main supergraph that reference the drive). The exact query structure will depend on your document models. 4. **Clear Caches and Reset (If Necessary):** - Sometimes, old state or cached data can cause confusion. As a general troubleshooting step if issues persist: - Try deleting the `.ph` folder in your user's home directory (`~/.ph`). This folder stores global Powerhouse configurations and cached dependencies. - Clear browser storage (localStorage, IndexedDB) for the Connect application. ### Expected outcome - You can correctly identify which GraphQL endpoint to use for different types of queries and operations. - You understand that document-specific operations (like creating or updating documents in a drive) are typically handled by drive-specific endpoints. - You can use browser developer tools to inspect network requests and confirm which endpoints Powerhouse Connect is using. - Documents sync as expected, and you can retrieve their state by querying the appropriate endpoint. ### Common issues and solutions - **Issue:** Documents created in Connect don't appear when querying `http://localhost:4001/graphql`. - **Solution:** You are likely querying the general supergraph. For document-specific data, ensure you are targeting the drive's endpoint (e.g., `http://localhost:4001/d/`) or using queries designed to fetch data from specific drives. Inspect Connect's network activity to see the endpoint it uses for `pushUpdates`. - **Issue:** Persistent syncing problems or unexpected behavior after trying the above. - **Solution:** Consider cleaning the global Powerhouse setup by removing `~/.ph`
Resetting Your Localhost Environment ### How to Reset Your Localhost Environment --- ### Problem statement You are running Powerhouse locally (via `ph vetra --watch` or `ph connect`), but you can't find your local drive in the interface. Alternatively, you can see the drive or have recreated it, but the `DocumentModel` button is missing, preventing you from creating new document model schemas. ### Prerequisites - Powerhouse Connect is running locally. - The Powerhouse Connect interface is open in your browser. ### Solution This issue is often caused by corrupted or inconsistent data stored in your browser's local storage for the Connect application. Clearing this storage forces a re-initialization of your local environment. ### Step 1: Open Settings In the bottom-left corner of the Powerhouse Connect UI, click on the "Settings" menu. ### Step 2: Find the Danger Zone In the settings panel that appears, scroll or navigate to the "Danger Zone" section. ### Step 3: Clear Local Storage Click the "Clear storage" button. A confirmation prompt may appear. Confirming this action will wipe all application data stored in your browser for your local environment, including the state of your local drive. ### Step 4: Verify the Reset The application should automatically refresh and re-initialize its state. If it doesn't, manually reload the page. Your local drive should now be present with the `DocumentModel` button restored. ### Expected outcome - Your local drive is visible again in the Powerhouse Connect UI. - The `DocumentModel` button is available inside the local drive. - You can proceed to create and edit document models in your local environment. ### Common issues and solutions - **Issue**: The problem persists after clearing storage. - **Solution**: Try clearing your browser's cache and cookies for the localhost domain. As a last resort, follow the recipe for [Clearing Package Manager Caches](#clearing-package-manager-caches) and reinstalling dependencies. ### Related recipes - [Troubleshooting Document Syncing](#troubleshooting-document-syncing) - [Initializing a New Project & Document Model](#initializing-a-new-project-and-document-model)
Clearing Package Manager Caches ### How to Clear Package Manager Caches --- ### Problem statement You are encountering unexpected issues with dependencies, `ph-cmd` installation, or package resolution. Corrupted or outdated caches for your package manager (pnpm, npm, yarn) can often be the cause. Clearing the cache forces the package manager to refetch packages, which can resolve these problems. ### Prerequisites - Terminal or command prompt access - A package manager (pnpm, npm, or yarn) installed ### Solution Choose the commands corresponding to the package manager you are using. ### For pnpm `pnpm` has a robust set of commands to manage its content-addressable store. ```bash # Verify the integrity of the cache pnpm cache verify # Remove orphaned packages from the store pnpm store prune ``` ### For npm `npm` provides commands to clean and verify its cache. ```bash # Verify the contents of the cache folder, which can fix some issues npm cache verify # If verification doesn't solve the issue, force clean the cache npm cache clean --force ``` ### For Yarn (v1 Classic) Yarn Classic allows you to list and clean the cache. ```bash # List the contents of the cache yarn cache list # Clean the cache yarn cache clean --force ``` ### Expected outcome - The package manager's cache is cleared or verified. - Subsequent installations will fetch fresh versions of packages, potentially resolving dependency-related errors. - Your system is in a cleaner state for managing Powerhouse project dependencies. ### Common issues and solutions - **Issue**: Problems persist after clearing the cache. - **Solution**: The issue might not be cache-related. Consider completely removing `node_modules` and lockfiles (`pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`) and running `pnpm install` (or equivalent) again. ### Related recipes - [Installing 'ph-cmd'](#installing-ph-cmd) - [Uninstalling 'ph-cmd'](#uninstalling-ph-cmd) - [Managing and Updating Powerhouse Dependencies](#managing-and-updating-powerhouse-dependencies)
--- ## Glossary > Source: https://powerhouse.academy/academy/Glossary ## General Terms - **Powerhouse** โ€“ A network organization that provides open-source software and services to support decentralized operations for other network organizations. - **Scalable Network Organization (SNO)** โ€“ A network organization structured according to the Powerhouse framework, designed for sustainable and scalable growth. - **Powerhouse Ecosystem** โ€“ The overall environment of Powerhouse tools, applications (like Connect), concepts (document models, packages), and services. ## Technology & Framework - **CQRS (Command Query Responsibility Segregation)** โ€“ A pattern that separates read and write operations to improve scalability. - **Event Sourcing** โ€“ A method of storing system state as a sequence of immutable events rather than modifying a single record. ## Software Components - **Model Context Protocol (MCP)** โ€“ A standardized protocol that enables AI agents and external tools to interact with systems through structured operations. Powerhouse uses MCP to provide AI access to document management capabilities. - **Reactor** โ€“ A storage node for Powerhouse documents and files with multiple storage adapters (local, cloud, decentralized). Reactors process mutations as jobs, emit events, and coordinate read models and processors. - **IReactorClient** โ€“ The primary programmatic interface for interacting with a reactor. Provides Promise-based methods for reading, creating, updating, deleting, and subscribing to documents. See the [IReactorClient API Reference](/academy/APIReferences/ReactorClient). - **Job (Reactor Job)** โ€“ A unit of work in the reactor's queue. Each mutation (e.g., executing actions on a document) becomes a job that moves through statuses: PENDING, RUNNING, WRITE_READY, READ_READY, or FAILED. - **ProcessorManager** โ€“ The reactor component that routes operations to user-defined processors. After a job reaches READ_READY, the ProcessorManager matches operations against each processor's filter and calls `onOperations()` on matching processors. - **Reactor-MCP** โ€“ A Model Context Protocol server for the Powerhouse ecosystem that provides AI agents and tools with structured access to document model operations, serving as a bridge between AI systems and Powerhouse document management infrastructure. - **Powerhouse Switchboard** โ€“ A scalable API service that aggregates and processes document data. - **Powerhouse Fusion** โ€“ A platform front-end that hosts the public marketplace for SNO interactions. - **Powerhouse Renown** โ€“ A decentralized authentication system managing contributor reputation. - **Powerhouse Academy** โ€“ A training platform for onboarding and upskilling SNO contributors. - **Connect** โ€“ The contributor's public or private workspace, serving as the entry point for individual contributors to install apps and packages for specific business solutions. - **Powergrid** โ€“ A decentralized network of reactors that sync with each other. - **Preview Drive** โ€“ A local drive created in `--watch` mode during `ph vetra` development, used for testing local document models and editors without affecting the main synced drive. - **Remote Drive** โ€“ A Powerhouse drive hosted on a remote server (e.g., Vetra) that syncs across team members, enabling collaborative development on shared documents and document models. - **Powerhouse CLI (ph)** โ€“ The command-line tool for Powerhouse project initialization, code generation, package management, and running local development environments (Connect Studio). It also manages services, ensuring the terminology aligns with the updated setup guide. - **Connect App (Connect Studio)** โ€“ The primary Powerhouse application for defining document models, building/testing editors (in Studio mode), and collaborating on documents. - **Document Tools** โ€“ Built-in features within Powerhouse applications (e.g., Connect) that assist with document management, inspection, and interaction, such as Operations History. - **Operations History** โ€“ A Document Tool in Connect providing a chronological, immutable log of all operations on a document for traceability. - **Studio mode** โ€“ The local development mode of the Connect App (`ph connect`), for real-time document model definition, editor building, and testing. - **Renown (Login Flow)** โ€“ The Powerhouse decentralized login process using an Ethereum wallet signature to generate/retrieve a user's DID for secure, pseudonymous actions. - **Powerhouse Switchboard (Verifier Role)** โ€“ A function of Powerhouse Switchboard that validates DIDs and credentials for operations submitted via Connect, ensuring they are authorized. ## Document Modeling - **Action Creators (for Document Operations)** โ€“ Auto-generated helper functions creating structured "action" objects for dispatching operations to a document model's reducer. - **Actions (Document Actions)** โ€“ Typed objects representing an intent to change a document's state, dispatched to reducers, containing an operation type and input data. - **API Integration (for Document Models)** โ€“ The capability of Document Models to connect with Switchboard API or external APIs, facilitating data exchange between Powerhouse applications and other systems. - **Data Analysis (with Document Models)** โ€“ Leveraging the structured data within Document Models, often via read models, to extract insights, generate reports, and perform analytics on operational and historical data. - **Dispatch (in Document Models)** โ€“ The act of sending an action (representing an operation) to a document model's reducer to trigger a state update. - **Document Model Specification** โ€“ The formal definition of a document model (state, operations), created in Connect Studio (using GraphQL SDL) and exported (e.g., `.phdm.zip`) for code generation. - **Document Models** โ€“ Structured data models that define how Powerhouse documents store and process information. - **Document State** โ€“ The current data held by a document instance, structured according to its Document Model. - **Document Type** โ€“ A unique string identifier (e.g., `powerhouse/todo-list`) for a Document Model, used by host apps to select the correct editor/logic. - **Event History (Append-Only Log)** โ€“ An immutable, append-only log where every operation applied to a Powerhouse document is stored as an event. It provides a transparent audit trail and enables features like time travel debugging and state reconstruction. - **GraphQL Scalars** โ€“ Data types used in Powerhouse document modeling (e.g., `String`, `Int`, `Currency`, `OID` for unique object IDs). - **GraphQL Schema Definition Language (SDL) (for Document Models)** โ€“ Language used in Connect Studio to define a Document Model's data structure (state) and operations. - **Immutable Updates** โ€“ A principle where data is never altered in place; operations create new data versions, vital for Powerhouse's event sourcing. - **Input Types (GraphQL for Document Operations)** โ€“ Custom data structures in SDL detailing parameters for document operations (e.g., `AddTodoItemInput`). - **Model-Driven Development (MDD)** โ€“ A software approach that uses high-level models to generate system logic and configurations. - **Operations (Document Operations)** โ€“ Named commands (e.g., `ADD_TODO_ITEM`) in a Document Model representing all ways to change its state, forming its event log. - **Powerhouse Document (`.phd` file)** โ€“ Standard file extension for an exported Powerhouse document instance, containing its data and history. - **Pure Functions (for Reducers)** โ€“ Principle that document model reducers must be pure (output depends only on input, no side effects) for predictable state transitions. - **Reducers (Document Model Reducers)** โ€“ Functions implementing a Document Model's logic; for each operation, a reducer takes current state and an action, returning new state. - **Replay Events** โ€“ The process of re-applying recorded events from a document's Event History to reconstruct or restore its state, a core capability of Event Sourcing. - **Scope** โ€“ A partition of a document's operation history. Operations are grouped by scope (e.g., `"global"`, `"local"`), allowing independent histories on the same document. Global scope contains shared state; local scope contains user-specific state. - **Branch** โ€“ A named fork of a document's operation history, enabling draft/published workflows. Operations are isolated per branch. The default branch is `"main"`. - **State (Global State in Document Model)** โ€“ The primary, persisted, shared data of a document instance, managed by its reducers. - **State Schema** โ€“ The component of a Document Model that defines the structure of the document, including its fields, data types, and validation rules, typically using a GraphQL-like syntax. It serves as a blueprint for how data is stored and validated. - **Strands** โ€“ A single synchronization channel that connects exactly one unit of synchronization to another, with all four parameters (drive_url, doc_id, scope, branch) set to fixed values. This allows synchronization between two distinct points of instances of a document or document drive. - **Time Travel Debugging** โ€“ A debugging technique, enabled by a document's Event History, that allows developers to reconstruct and inspect past states of the document by replaying events up to a specific point in time. - **Type Safety (in Document Modeling)** โ€“ Powerhouse's use of auto-generated TypeScript definitions from a model's schema (SDL) to prevent data type errors in development. - **Version Control (for Document Models)** โ€“ A planned feature for Document Models in Connect that will allow tracking of changes, comparison of different versions, and maintenance of data integrity over time, similar to version control systems for source code. ## Development & Tooling - **Boilerplate (Powerhouse Project)** โ€“ The `ph init` command's initial project structure, providing a standard starting point for new Powerhouse packages. - **Connect Build** โ€“ The output of the `ph connect build` command, which packages a Connect project into a distributable format. This build includes all necessary local/external packages, assets, and styles, and can be previewed locally with `ph connect preview` or deployed. - **Development Environment (Powerhouse)** โ€“ A local setup for developing Powerhouse applications. Use `ph reactor` to start the backend services (Powerhouse Switchboard) for real-time document model processing, code generation, and live updates, and `ph connect` to start the front-end Connect Studio. These can be run independently or together in separate terminals. - **Document Model Editors** โ€“ An interface or UI to a document model that allows users to create and modify the data captured by the document models. - **Drive** โ€“ A logical container in Powerhouse for storing, organizing, and managing collections of documents. - **Drive-app** โ€“ A UI application, often custom, providing tailored views and interactions with documents in a drive. - **Environments (Powerhouse Environments)** โ€“ Pre-defined configurations for a project's Powerhouse dependencies, such as `dev` (development), `prod` (production/latest), and `local`. The Powerhouse CLI (`ph use` command) allows developers to easily switch between these environments to use different versions of packages (e.g., bleeding-edge, stable, or from a local monorepo). - **Host Applications** โ€“ Applications that use the Powerhouse framework to create and manage documents and data. - **Modules (in Document Model Editor)** โ€“ An organizational feature in Connect Studio's model editor for grouping related operations. - **Powerhouse Package** โ€“ A collection of document models, document model editors, and other resources that are published as a package and can be used in any of the host applications. - **Powerhouse Project** โ€“ A collection of document models, document model editors, and other resources being build in Connect Studio. - **Scalars (Design System Components)** โ€“ Reusable UI building blocks (e.g., `Checkbox`, `InputField`) from `@powerhousedao/document-engineering/scalars`, used in editors (distinct from GraphQL scalars). - **State (Local State in Editor)** โ€“ Temporary, UI-specific data within an editor (e.g., form inputs), not persisted in the global document state. - **Storybook (for Powerhouse Design System)** โ€“ Interactive environment for browsing and testing Powerhouse Design System UI components. - **Tailwind CSS (in Connect Studio)** โ€“ Utility CSS framework integrated into Connect Studio for styling document editors. - **Vetra** โ€“ A Powerhouse platform for hosting remote drives, enabling collaborative development and document synchronization across team members. - **Watch Mode (`--watch`)** โ€“ A development mode flag for `ph vetra` that enables dynamic loading of local document models and editors, creating a separate Preview Drive for testing changes in real-time. ## AI & Automation - **AI Assistants** โ€“ AI-powered contributors paired with human contributors to automate tasks and improve productivity. - **Document Model Agent** โ€“ A specialized AI agent that guides users through creating document models, handling requirements gathering, design confirmation, and implementation using MCP tools for state schema definition, operation creation, and code generation. - **AI Contributor Modes** โ€“ Configurable states that determine the AI assistant's behavior, permissions, and task focus. - **Task Automation & Scaling** โ€“ The use of AI to streamline repetitive tasks, improve communications, and enhance decision-making. - **Decentralized Identifier (DID)** โ€“ A user-controlled, globally unique ID, used in Renown to link a user's blockchain key to actions pseudonymously. ## Organizational Concepts - **Ceramic** โ€“ A decentralized network for storing verifiable data, used by Powerhouse Renown for secure credential management. - **Decentralized Identifier (DID)** โ€“ A user-controlled, globally unique ID, used in Renown to link a user's blockchain key to actions pseudonymously. - **Event-Driven Architecture (EDA)** โ€“ A software design approach where system flows are determined by events that trigger actions asynchronously. - **Network Organization** โ€“ A group of independent contributors and teams working together towards a common purpose, relying on decentralization and resource sharing. --- ## LLM docs > Source: https://powerhouse.academy/academy/LLMDocs The Powerhouse Academy follows the [llms.txt standard](https://llmstxt.org) to provide machine-readable documentation for LLMs and AI coding tools. ## Files | File | Purpose | Size | | ------------------------------- | ----------------------------------------------------------------------- | ----- | | [llms.txt](/llms.txt) | Navigation index โ€” links to every doc page with a short description | ~22KB | | [llms-full.txt](/llms-full.txt) | All documentation concatenated into one file for full-context ingestion | ~1MB | ### llms.txt A concise index that follows the [llms.txt spec](https://llmstxt.org): an H1 title, a summary blockquote, and H2 sections with links to every page. Ideal for tools that fetch context on demand. ### llms-full.txt The complete academy content in a single markdown file. Use this when you want to load the full documentation into a context window at once (e.g. pasting a URL into Claude or ChatGPT). ## Programmatic access ```bash # Index (llms.txt) curl https://powerhouse.academy/llms.txt # Full content (llms-full.txt) curl https://powerhouse.academy/llms-full.txt ``` ## Regenerating the files The files are generated from the academy source docs by the script at `scripts/generate-llm-docs.ts`. Run it with: ```bash npm run generate:llm-docs ``` This writes both `static/llms.txt` and `static/llms-full.txt`, which Docusaurus serves at the site root. The legacy `academy_LLM_docs.md` path is also kept for backward compatibility. --- ## Step 2 โ€” Implement the `TodoList` document model reducer operation handlers > Source: https://powerhouse.academy/academy/TodoListTutorial/ImplementTodoListDocumentModelReducerOperationHandlers ## Adding the logic for handling operations with reducers Your document model update's the state of a given document by applying a set of append-only actions. Once these have been applied to the document, we call them operations. The document model does this with a reducer โ€” a function which takes the existing state and a given action, and then returns the new state with the action applied. ## What we have so far The operation handler logic for each module is found in `document-models/SOME-DOCUMENT-MODEL/src/reducers/SOME-MODULE-NAME.ts`. So for our todos module, we will implement our handler logic in `document-models/todo-list/src/reducers/todos.ts` When you generated your document model code, we created a boilerplate implementation of the reducer logic for each of the operations we defined in step 1. You will see that there are functions for handling each of the operations, but all they do is throw "not implemented" errors. ```ts export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` Let's add the handler logic for each operation in the same order we defined them in the previous step. To handle the `addTodoItemOperation`, all we need to do is create an `id` for our new operation, and then push an object with that `id` and the rest of the action input into the `items` array in our state. Update your `addTodoItemOperation` like so: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { // removed-start addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, // removed-end // added-start addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, // added-end updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` Under the hood, we use a library for making the functions always create and return new copies of the state, i.e. they are always _immutable_. This is why you don't actually have to return your new state, the newly created copy of the state is used automatically. The `updateTodoOperation` works in much the same way, except this time instead of creating a new `id`, we find the item in the items array which has the given id. Then we spread out the rest of the values we get from the action input, same as when creating. Update your `updateTodoOperation` to be like so: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, // removed-start updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, // removed-end // added-start updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // added-end deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` The delete operation is the simplest of the three. All we need to do is filter the items array so that it no longer contains the item with the given id. ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // removed-start deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, // removed-end // added-start deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, // added-end }; ``` With that all done, your final result should look like this: ```ts export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-2-complete-implemented-todo-list-document-model-reducer-operation-handlers` ### Up next: tests for our new operation handlers Up next, you'll implement some custom tests to check the behavior of our new code. --- ## Step 3 โ€” Adding our own tests for the document model actions > Source: https://powerhouse.academy/academy/TodoListTutorial/AddTestsForTodoListActions Similarly to the operation handler logic, when you add a new module to your document model, we generate some boilerplate tests for your code. Take a look in `document-models/todo-list/src/tests/todos.test.ts` You will see that we have some basic "sanity check" style tests for you already. These make sure that your operations are at least able to result in a valid document model state. You should copy these boilerplate checks in your other tests to ensure that your outputs are valid. ```ts /** * This is a scaffold file meant for customization: * - change it by adding new tests or modifying the existing ones */ reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { // the `createDocument` utility function from your document model creates // an a new empty document, i.e. one with your default initial state const document = utils.createDocument(); // the generate mock function takes one of your generated input schemas // and creates an object populated with random values for each field const input = generateMock(AddTodoItemInputSchema()); // we call your document model's reducer with the new document we just created // and the action we want to test, `addTodoItem` in this case // the reducer returns a new object, which is the document with the action applied // if successful, there will be an operation which corresponds to this action // in the updated document's operations list const updatedDocument = reducer(document, addTodoItem(input)); // when you generate a document model, we give you some validation utilities like // `isTodoListDocument` which confirms the document is of the correct form in a way // that typescript recognizes expect(isTodoListDocument(updatedDocument)).toBe(true); // at the start a document will have 0 operations, so after applying this action // there should now be one operation expect(updatedDocument.operations.global).toHaveLength(1); // the operation added to the list should correspond to the correct action type expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); // the operation added should have used the correct input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // the index of the operation should be 0, since it is the first and only operation expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation", () => { // ... }); it("should handle deleteTodoItem operation", () => { // ... }); }); ``` Since testing the `addTodoItemOperation` is such a simple case, we have not added further testing here. You are welcome to add a more test cases for it if you want. ## Tests for update operations ### Test updating the todo item text Let's add some more sophisticated tests for our `updateTodoItem` operation. We want to know that we can update todos successfully, and that we we do so it only changes the values we want to change, while leaving the rest as is. Delete the existing "should handle updateTodoItem operation" test. ```typescript // removed-start it("should handle updateTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(UpdateTodoItemInputSchema()); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); // removed-end ``` Let's test that the text of a todo item is updated correctly first. Put this code in the place where you just deleted the existing test case: ```ts it("should handle updateTodoItem operation to update text", () => { // we need there to already be a todo item in the document, // since we want to test updating an existing document const mockItem = generateMock(TodoItemSchema()); // we also need to generate a mock input for the update operation we are testing const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // since the mocks are generated with random values, we need to set the `id` on our mock input // to match the `id` of the existing mock input input.id = mockItem.id; // we want to easily check if the item's text was updated to be our new value, // so we assign a variable and use that for the mock input's text field const newText = "new text"; input.text = newText; // we are only testing updating the text here, so we want the checked field on the input // to be undefined, i.e. it should not change anything on the existing item input.checked = undefined; // we can pass a different initial state to the `createDocument` utility, // so in this case we pass in an `items` array with our existing item already in it const document = utils.createDocument({ global: { items: [mockItem], }, }); /* The following checks are copied from the boilerplate */ // create an updated document by applying the reducer with the action and input const updatedDocument = reducer(document, updateTodoItem(input)); // use our validator to check that the document conforms to the document model schema expect(isTodoListDocument(updatedDocument)).toBe(true); // there should now be one operation in the operations list expect(updatedDocument.operations.global).toHaveLength(1); // the operation applied should correspond to an action of the correct type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // the operation applied should have used the correct input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // the operation applied should be the first operation in the list expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // find the updated item in the items list by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's text should now be updated to be our new text expect(updatedItem?.text).toBe(newText); // the item's `checked` field should be unchanged. expect(updatedItem?.checked).toBe(mockItem.checked); }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ### Test updating the todo item checked state Now let's do the same thing, but for the checked state of an item. This test is essentially just the same as the above, but we update the `checked` field while leaving the `text` field `undefined`. Add this code below the test case we just added: ```ts it("should handle updateTodoItem operation to update checked", () => { // generate a mock existing item const mockItem = generateMock(TodoItemSchema()); // generate a mock input const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // set the mock input's `id` to the mock item's `id` input.id = mockItem.id; // we want the new `checked` field value to be the opposite of the randomly generated value from the mock const newChecked = !mockItem.checked; input.checked = newChecked; // leave the `text` field unchanged input.text = undefined; // create a document with the existing item in it const document = utils.createDocument({ global: { items: [mockItem], }, }); // apply the reducer with the action and the mock input const updatedDocument = reducer(document, updateTodoItem(input)); /* The following checks are copied from the boilerplate */ // validate your document expect(isTodoListDocument(updatedDocument)).toBe(true); // check your operations expect(updatedDocument.operations.global).toHaveLength(1); // check the operation's action type expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // check the operation's input expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); // check the operation's index expect(updatedDocument.operations.global[0].index).toEqual(0); /* The following checks are unique to this test case */ // get the updated item by it's `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // the item's `text` field should remain unchanged expect(updatedItem?.text).toBe(mockItem.text); // the item's `checked` field should be updated to our new checked value expect(updatedItem?.checked).toBe(newChecked); }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ## Test for deleting todo items You will have seen that the tests for the `deleteTodoItem` operation passed, even though we didn't set up an existing item to delete. This is because the boilerplate just checks that the operation was applied with the correct inputs, which it technically was. Checking that it actually had the _result_ we want is our job. Update the `deleteTodoItem` operation test case to also create an existing item and then check that is was actually deleted: ```typescript it("should handle deleteTodoItem operation", () => { // removed-start const document = utils.createDocument(); const input = generateMock(DeleteTodoItemInputSchema()); // removed-end // added-start const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock(DeleteTodoItemInputSchema()); input.id = mockItem.id; // added-end const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); // added-start const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); // added-end }); ``` #### Check your work Running `pnpm tsc && pnpm lint && pnpm test` should pass ## Final result After these updates, your `document-models/todo-list/src/tests/todos.test.ts` file should look like this: ```ts /** * This is a scaffold file meant for customization: * - change it by adding new tests or modifying the existing ones */ AddTodoItemInput, DeleteTodoItemInput, UpdateTodoItemInput, } from "todo-tutorial/document-models/todo-list"; reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, TodoItemSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newText = "new text"; input.text = newText; input.checked = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(newText); expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newChecked = !mockItem.checked; input.checked = newChecked; input.text = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(mockItem.text); expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock( DeleteTodoItemInputSchema(), ); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-3-complete-implemented-tests-for-todo-operations` ### Up next: generating an editor for our `TodoList` documents Up next, we'll generate a boilerplate document editor for our `TodoList` documents. --- ## Step 5 โ€” Implement `TodoList` document editor UI components > Source: https://powerhouse.academy/academy/TodoListTutorial/ImplementTodoListDocumentEditorUIComponents Out of the box, we have a component for updating our `TodoList` documents' names, but we would like to create, read, update, and delete all of the data in our documents. ## Add a component for showing our todo list in the document editor Let's start by adding a `` component that will be the main container we show when you open a TodoList document. Create a new file at `editors/todo-list-editor/components/TodoList.tsx` and add this: ```jsx /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; return (
{JSON.stringify(selectedTodoListDocument)}
); } ``` We've moved the `` component here, so replace it in `editors/todo-list-editor/editor.tsx` with this component we just created. ## Adding the Document Toolbar The `DocumentToolbar` component provides essential document operations like saving, sharing, and navigation. To add it to your document editor, simply import it from the design system and place it at the top of your editor component. The toolbar automatically connects to the currently selected document and provides all standard document actions. For more details, see the [DocumentToolbar documentation](../docs/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx). ```tsx // removed-line // added-line export default function Editor() { return (
// removed-line // added-line
); } ``` Now when you open a TodoList document in Connect, you will see an (albeit ugly for now) representation of your whole document in JSON. ## Add a component for adding todo items to a todo list Next, let's add a component for adding todos to a todo list. Create a new file at `editors/todo-list-editor/components/AddTodo.tsx` and add this to it: ```jsx export function AddTodo() { // The hooks for getting documents also return a dispatch function for dispatching actions to modify the document. // This is the same pattern you will have seen in React's `useReducer` hook, except you don't need to pass the initial state. // The document we are working with _is_ the initial state. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const onSubmitAddTodo: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const addTodoInput = form.elements.namedItem("addTodo") as HTMLInputElement; const text = addTodoInput.value; if (!text) return; dispatch(addTodoItem({ text })); form.reset(); }; return (
); } ``` We have provided some basic Tailwind styles but you are welcome to style your components however you wish. This hooks and functions also work with other component libraries like Radix etc. Let's add this component to our `` component. ```tsx // added-line /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; return (
// added-line
{JSON.stringify(selectedTodoListDocument)}
); } ``` Now when you open a TodoList document in Connect, you can add more todos. ## Add a button for closing the open `TodoList` document Of course it's all well and good to be able to open TodoList documents, but we would also like to be able to close them. Let's add a `` component that closes the selected document when clicked. Create a new file at `editors/todo-list-editor/components/CloseButton.tsx` and add this content: ```jsx /** Closes the selected todo list document editor */ export function CloseButton() { const onCloseButtonClick: MouseEventHandler = () => { // this function sets the selected node in Connect. // a node can be either a file or a folder, and the same function works for both. // notably, this is not a hook and therefore does not need to abide by the rules of hooks. setSelectedNode(undefined); }; return ( ); } ``` Let's add this component to our `` component: ```tsx // added-line /** Displays the selected todo list */ export function TodoList() { // this hook returns the currently selected TodoList document const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; return (
// added-line
{JSON.stringify(selectedTodoListDocument)}
); } ``` Now you have a button you can click to close the selected document. ## Add components for todo items and the list of todo items Finally, let's add a component for showing and editing and individual todo item in a todo list, and another one for showing the list of todo items. Create a new file at `editors/todo-list-editor/components/Todo.tsx` and add this content: ```jsx useState, type ChangeEventHandler, type FormEventHandler, type MouseEventHandler, } from "react"; deleteTodoItem, updateTodoItem, } from "todo-tutorial/document-models/todo-list"; type Props = { todo: TodoItem; }; /** Displays a single todo item in the selected todo list * * Allows checking/unchecking the todo item. * Allows editing the todo item text. * Allows deleting the todo item. */ export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); // even though this component is for a todo item and not a whole list, we can use the exact same hook for dispatching updates to it. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const todoId = todo.id; const todoText = todo.text; const todoChecked = todo.checked; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; // we can use this dispatch function for any of the actions supported by a TodoList document dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch( updateTodoItem({ id: todo.id, checked: event.target.checked, }), ); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todoId })); }; const onClickEditTodo: MouseEventHandler = () => { setIsEditing(true); }; const onClickCancelEditTodo: MouseEventHandler = () => { setIsEditing(false); }; if (isEditing) return (
); return (
{todoText}
); } ``` Now create another new file at `editors/todo-list-document/Todos.tsx` and give it this content: ```jsx type Props = { todos: TodoItem[]; }; /** Shows a list of the todo items in the selected todo list */ export function Todos({ todos }: Props) { const hasTodos = todos.length > 0; if (!hasTodos) { return

Start adding things to your todo list

; } return (
    {todos.map((todo) => (
  • ))}
); } ``` And replace the content of your `TodoList.tsx` file with this: ```jsx /** Displays the selected todo list */ export function TodoList() { const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const todos = selectedTodoList.state.global.items; return (
); } ``` ``` editors/todo-list-editor/ โ”œโ”€โ”€ components/ โ”‚ โ””โ”€โ”€ EditName.tsx # Auto-generated component for editing document name โ”œโ”€โ”€ editor.tsx # Main editor component (do not change this) โ””โ”€โ”€ module.ts # Editor module export (do not change this) ``` ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to open a `TodoList` document and update all of the fields we defined in the `TodoList` document model schema - make sure your code matches the code in the completed step branch run: `git diff your-branch-name step-5-complete-added-basic-todo-list-document-editor-ui-components` ## Up next: generating a custom drive explorer for managing our `TodoList` documents Next, we will generate a special kind of editor called a "drive editor" which we will use instead of the generic drive explorer. --- ## Step 7 - Add shared component for showing TodoList stats > Source: https://powerhouse.academy/academy/TodoListTutorial/AddSharedComponentForShowingTodoListStats So far we've been creating components that live in the same directories as the editors that use them. But sometimes we want to use the same component across multiple editors. Let's create a component for showing statistics about our todos. We'd like this component to work with any set of todos or todo lists, so that we can use the same one in our document editor or in our drive editor or a folder. ## Creating the `` component Create a new directory at `editors/components` and create two new files inside it: `editors/components/Stats.tsx` with this content: ```jsx TodoItem, TodoListDocument, } from "todo-tutorial/document-models/todo-list"; type Props = { todos: TodoItem[] | undefined; todoListDocuments?: TodoListDocument[] | undefined; createdAtUtcIso?: string; lastModifiedAtUtcIso?: string; }; /** Generic component for showing statistics about todo lists and the todos they contain */ export function Stats({ todos, todoListDocuments, createdAtUtcIso, lastModifiedAtUtcIso, }: Props) { const totalTodos = todos?.length ?? 0; const totalChecked = todos?.filter((todo) => todo.checked).length ?? 0; const totalUnchecked = todos?.filter((todo) => !todo.checked).length ?? 0; const percentageChecked = Math.round( calculatePercentage(totalTodos, totalChecked), ); const percentageUnchecked = Math.round( calculatePercentage(totalTodos, totalUnchecked), ); const createdAt = createdAtUtcIso ? new Date(createdAtUtcIso) : null; const hasCreatedAt = createdAt !== null; const lastModified = lastModifiedAtUtcIso ? new Date(lastModifiedAtUtcIso) : null; const hasLastModified = lastModified !== null; const createdAtFormattedDate = createdAt ? createdAt.toLocaleDateString() : null; const lastModifiedFormattedDate = lastModified ? lastModified.toLocaleDateString() : null; const createdAtFormattedTime = createdAt ? createdAt.toLocaleTimeString() : null; const lastModifiedFormattedTime = lastModified ? lastModified.toLocaleTimeString() : null; const totalTodoListDocuments = todoListDocuments?.length ?? 0; const hasTodoLists = todoListDocuments !== undefined; return (
    {hasTodoLists && (
  • Todo Lists: {totalTodoListDocuments}
  • )}
  • Todos: {totalTodos}
  • Checked:{" "} {totalChecked} ({percentageChecked}%)
  • Unchecked:{" "} {totalUnchecked} ({percentageUnchecked}%)
  • {hasCreatedAt && (
  • Created:{" "} {createdAtFormattedDate} {createdAtFormattedTime}
  • )} {hasLastModified && (
  • Last modified:{" "} {lastModifiedFormattedDate} {lastModifiedFormattedTime}
  • )}
); } function calculatePercentage(total: unknown, value: unknown) { if (typeof total !== "number" || typeof value !== "number") { return 0; } const ratio = value / total; if (isNaN(ratio)) { return 0; } return ratio * 100; } ``` And `editors/components/index.ts` with this content: ```ts export { Stats } from "./Stats.js"; ``` The index file lets us use a nice neat import path like `todo-tutorial/editors/components` in all of our editor components. Don't be too concerned with the math and time related code you see here โ€” those are just implementation details. ## Using the `` component in our `TodoListEditor` Now let's use the `` component in our `` component: ```diff + import { Stats } from "todo-tutorial/editors/components"; /** Displays the selected todo list */ export function TodoList() { const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const todos = selectedTodoList.state.global.items; + const createdAtUtcIso = selectedTodoList.header.createdAtUtcIso; + const lastModifiedAtUtcIso = selectedTodoList.header.lastModifiedAtUtcIso; return (
+
+ +
); } ``` With this, you will now see statistics about the todo items in a todo list document. And now we can also show off the flexibility of our new `` component. Since drives are also just documents themselves, we can derive the same information about a drive too. This means we can use this same component in our drive editor as well. ## Using the `` component in our `TodoDriveExplorer` Let's add this to our `` component, along with some conditional logic that either shows stats for the selected folder (if one is selected) or the selected drive otherwise. ```diff + import { + useSelectedDrive, + useSelectedFolder, + } from "@powerhousedao/reactor-browser"; + import { Stats } from "todo-tutorial/editors/components"; + import { + useTodoListDocumentsInSelectedDrive, + useTodoListDocumentsInSelectedFolder, + type TodoItem, + type TodoListDocument, + } from "todo-tutorial/document-models/todo-list"; + /** Small helper function to get all todo items from all todo lists */ + export function getAllTodoItemsFromTodoLists( + todoLists: TodoListDocument[] | undefined, + ): TodoItem[] { + return todoLists?.flatMap((todoList) => todoList.state.global.items) ?? []; + } /** Shows the documents and folders in the selected drive */ export function DriveContents() { + const selectedFolder = useSelectedFolder(); + const hasSelectedFolder = selectedFolder !== undefined; return (
+ {hasSelectedFolder ? : }
); } + /** Shows the statistics for the selected drive */ + function DriveStats() { + const todoListDocumentsInSelectedDrive = + useTodoListDocumentsInSelectedDrive(); + const allTodos = getAllTodoItemsFromTodoLists( + todoListDocumentsInSelectedDrive, + ); + const [selectedDrive] = useSelectedDrive(); + const driveCreatedAt = selectedDrive.header.createdAtUtcIso; + const driveLastModified = selectedDrive.header.lastModifiedAtUtcIso; + + return ( + + ); + } + + /** Shows the statistics for the selected folder */ + function FolderStats() { + const todoListDocumentsInSelectedFolder = + useTodoListDocumentsInSelectedFolder(); + const allTodos = getAllTodoItemsFromTodoLists( + todoListDocumentsInSelectedFolder, + ); + + return ( + + ); + } ``` The final result should look like this: ```jsx useSelectedDrive, useSelectedFolder, } from "@powerhousedao/reactor-browser"; useTodoListDocumentsInSelectedDrive, useTodoListDocumentsInSelectedFolder, type TodoItem, type TodoListDocument, } from "todo-tutorial/document-models/todo-list"; /** Small helper function to get all todo items from all todo lists */ export function getAllTodoItemsFromTodoLists( todoLists: TodoListDocument[] | undefined, ): TodoItem[] { return todoLists?.flatMap((todoList) => todoList.state.global.items) ?? []; } /** Shows the documents and folders in the selected drive */ export function DriveContents() { const selectedFolder = useSelectedFolder(); const hasSelectedFolder = selectedFolder !== undefined; return (
{hasSelectedFolder ? : }
); } /** Shows the statistics for the selected drive */ function DriveStats() { const todoListDocumentsInSelectedDrive = useTodoListDocumentsInSelectedDrive(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedDrive, ); const [selectedDrive] = useSelectedDrive(); const driveCreatedAt = selectedDrive.header.createdAtUtcIso; const driveLastModified = selectedDrive.header.lastModifiedAtUtcIso; return ( ); } /** Shows the statistics for the selected folder */ function FolderStats() { const todoListDocumentsInSelectedFolder = useTodoListDocumentsInSelectedFolder(); const allTodos = getAllTodoItemsFromTodoLists( todoListDocumentsInSelectedFolder, ); return ( ); } ``` With this update, you can now see the statistics for the todo lists and todo items for the selected drive, folder or document depending on which you select. ## Check your work To make sure all works as expected, we should: - check types run: `pnpm tsc` - check linting run: `pnpm lint` - check tests run: `pnpm test` - test in connect run: `pnpm connect` โ€” you should now be able to see the `` component showing the data for your drives, folder and documents. - make sure your code matches the code in the completed step branch run: `git diff step-7-complete-added-shared-component-for-showing-todo-list-stats` ## The end Congratulations! You now have a working `TodoList` document model, and editor for those documents, and a drive editor for managing those documents. This will make a good starting point for creating your own new implementations. We're excited to see what you build! ---