Pivot Platform Concepts

Intro to Pivot for Technical People

Pivot is for Making Worlds

Why is that our tagline? Because our goal with the product goes far behind collaborative document editing. We're trying to be the best place for communities and organizations to create together, and yes, it starts with collaborating on content and messaging.

This article serves as an overview of Pivot the product, generally describing the key concepts from a user-facing standpoint.

Accessing the Application

The Pivot app is available on web, iOS/Android, and Mac/Windows. The iOS/Android apps are on the App Store/Google Play store however the Mac/Windows apps are not and must be downloaded from our marketing site.

To understand web app access read the Domains and DNS article.

Key Entities

User

Pivot users are the primary entity in Pivot, not organizations. That is to say, users are not scoped to an organization (a tenant) but rather have relationships with organizations and independently, with spaces.

This means that, by default, a user cannot be deleted or suspended by another user (only we can do this, using PivotAdmin). Organization Admins (those with Organization Roles) can add/remove users from their organizations spaces by creating and updating Space Memberships, but do not have any fundamental control of the users.

At the Enterprise tier, organizations can verify ownership of domains, and therefore can effectively control users who have a verified email address at such a verified domain. This creates the experience of a more traditional tenant-based SaaS experience.

Users can have multiple verified email addresses and can authenticate with any of them, whether via email link, Google or SAML SSO. SAML depends on the organization-level domain verification system mentioned above.

Users cannot add an email address if the email domain is one of an organization's ownedEmailDomains. An email address that uses such a domain can only be added by that email address being sent an invite, and Facebox enforces that such invites cannot be accepted by an existing user, only by the creation of a new user.

Likewise, a user cannot remove an email if that email domain is in an organization's ownedEmailDomains and must use that email as their preferred email.

In addition to a user being a member of multiple organizations (again, not through any sort of hard organization boundry, but simply by being a member of one or more of the organization's spaces) our client apps also support multiple users being logged in. In this case, a single mobile app/desktop app window/browser tab is scoped to one of the active user sessions.

Invites

Invites are the responsibility of Facebox. Visa and Friend share responsibility for serving as the API layer, as described in the Visa article.

Because an invite implies a future space or room membership or a combination of multiple of those, Facebox interfaces with Blockhead and Messenger as part of the acceptance process.

Creating an invite is actually done through Blockhead or Messenger, which then interface with Facebox. That is to say, Facebox exposes an API that Friend and Visa can use as part of accepting an invite, but it is Blockhead and Messenger that own the authorization logic for creating an invite. Facebox aggregates pending invite operations from both services into a single record that can be accepted.

Organization

Organization Roles and Groups

Organization Subscriptions, Tiers, and Features

Space

A space is the fundamental unit of collaboration in Pivot. Organizations are Pivot's customers, but spaces are how a group of users actually create a place to collaborate. Spaces have members and members have roles.

Each space exists inside an organization, which determines 1) who is billed for the full members in the space, 2) which organization admins can change the settings for the space and add/remove members, and 3) what features are available to the space.

Beyond a name, cover photo, members, and some settings, paces are effectively devoid of content. It's the blocks that exist inside the space that give it form and structure.

Block

Spaces are made up (almost) entirely of blocks.

Some block types can be changed into another. For example, a Text block can be changed into a numbered list item. However, an Assignment block can't be changed into anything.

Blocks can be children of other blocks. So a block can have both Block Responses related to it (comments, etc.) and other child blocks.

For example, an Event block can have Block Responses (RSVPs) and child blocks (text, headings, etc. that form the description of the event).

Additionally, blocks are used to construct the (optional) description of each space, though this is limited to a specific set of basic block types.

Blocks can also be parented to a message -- where the block has no direct relationship to a space, and exists as a sort of 'attachment' to a message sent in a room.

Blocks need to considered across services, as follows:

  • Blockhead needs to determine the schema and validation for the block type and recognize the type enum value. This includes validation for BlockData, BlockResponse, BlockView and other related entities. Blockhead needs to consider what other types this type can be converted to and visa-versa as well as what parents it can have and what contexts it can exist in (space, space description, room).

  • Friend needs to recognize the type enum value, but otherwise doesn't really know or care about the validation details specific to that type. Each block type has to be supported as:

  • Frontend applications need to consider much of the validation logic that Blockhead determines (e.g. whether and in what contexts this block appears in the new block menu) and need UI components for creating, editing, and viewing the block and the associated BlockView, BlockData, and BlockResponse records.

  • Buzzbuzz needs to consider the language and format of notifications for blocks of this type, if applicable.

  • Wallstreet needs to consider whether the block type represents a new SubscribableFeature potentially.

Block Types Google Sheet (opens in a new tab)

The feature set provided by blocks involves more than just the Block entity. Related entities extend the block based model.

Mutating Blocks

Moving Blocks

Friend provides a moveBlock mutation which fronts Blockhead's moveBlock gRPC method.

moveBlock will fail if:

  1. The new parent does not support children of the block type that the block being moved is
  2. AND there is not an obvious block type change that can be inferred.

Inferred block type changes are:

  1. Database Item -> Page (and reverse)
  2. Goal -> Page (and reverse)
  3. Event -> Page (but not reverse)

Additionally, moveBlock will fail if the acting user does not have edit permissions to the block being moved, the old parent, and the new parent.

Updating Blocks

Other than moving a block with moveBlock and creating one with createBlock, Blockhead (via Friend RPCs) provides two mutations for updating block values:

  1. Updates to the richTextContent field can be made with the binaryBlockUpdate mutation/method.
  2. Atomic updates can be made to any other field using UpdateBlockProperty, which takes a field name and a new value. Keeping each field update atomic reduces the likelihood of end-user confusion by increasing the likelihood that concurrent changes from multiple clients don't overwrite and can be 'merged together' without anytime of conflict resolution.

Block Data

Block View

User Block Data

A user has at most one User Block Data record per block. It is used to store information like when the user last viewed the block, whether they have 'checked it off', and their most recent configuration when they last viewed the block (e.g., what view they were using).

Block Responses

While User Block Data records are 1:1 between a user and a block, there are also cases where a user should be able to comment on/vote on/respond to a block (1:many User to Block Response).

A Block Response is different from a child block because it represents user-specific activity on the block, not a parent-child relationship of blocks containing blocks.

A Block Response is different from Block Data, because it is user-specific while Block Data reflects a distinct property of the block.

For each block type, Responses are permitted in a specific way (or not permitted at all). Every Block Response has a related block, but not all block types permit any Block Responses, or permit them to be created/updated in all block states. The block type determines the shape of the allowed Block Responses (e.g. comments/poll votes/assignment submissions/etc.).

This entity is similar to the Message entity in the Messenger service, but is owned by the Blockhead service and specifically designed to integrate with specific block types and provide validation in the context of the related block. Because Block Responses can include things like rich text comments and comment reactions, we do end up reimplementing similar logic in Blockhead, but that's necessary to maintain clear domain separation. The Block Response entity is deceptively different from the Message entity. For example, a POLL block cannot have comments, but it can have votes. That's still a Block Response, but it is nothing like a traditional message and its valid and invalid states are highly dependent on context from the block. Additionall, Block Response CRUD permissions are entirely based on Block context and concepts.

Room

Rooms are similar to the concept of channels in other messaging apps. The contain a time ordered series of messages, where each message can potentially be parented to another, therefore creating a thread.

Rooms have their own Room Memberships. However, rooms can also be linked to a space and a block in that space that represents the room's 'location' in the space. In this case, it is considered a Space Room, and therefore has exactly zero Room Memberships, because the Messenger service depends on the Blockhead service to provide authorization for the room and its contents.

If a room is not related to a space, it is either a direct room or a group room. Regardless of whether the room is group (and therefore has no related space, but can have more than two total room memberships) or space (and therefore is related to a blockId and has zero room memberships), it has a type. If a room is direct, then that is its type.

Rooms can represent audio or video or video connections. In video, audio, and streaming rooms, users send and recieve audio/video data when they 'enter' a room, not just messages. In chat and post rooms, the experience is limited to exchanging messages by default, but the room can still optionally represent the container for initiating an audio/video connection.

Specifically, there are five distinct types of rooms:

Direct Rooms

  • This is our 'Direct Messaging' implementation. Direct rooms have exactly two Room Memberships.
  • The primary experience of a direct room is chat, however they also provide audio and video communication.

Chat Rooms

  • Pivot chat rooms are intended to feel familiar. Messages can be threaded, users can react to messages, send read receipts, etc.
  • Chat rooms also support audio and video messages. Where a audio or video file is attached to a message.

Post Rooms

  • Post rooms have very similar features to chat rooms, however they are not intended to feel real-time or necessitate rapid responses. Post rooms feel more like a forum then texting.
  • Messages can be threaded, users can react to messages, send read receipts, etc.
  • Post rooms also support audio and video messages. Where a audio or video file is attached to a message.

Audio Rooms

  • Audio rooms and video rooms use the same underlying WebRTC system. The difference is that the UI is optimized for audio and that there is no way to send or receive a video signal.
  • Audio rooms can be recorded and those recordings are parented to the room.
  • In space rooms, audio rooms have audience vs. stage logic and also the ability to hide recordings.

Video Rooms

  • In addition to the features of audio rooms, video rooms allow sending and receiving video and screen-sharing.

Streaming Rooms

  • While streaming rooms feel similar to video rooms, there is no option to toggle recording on or off. Instead, a streaming room is either live or not live.
  • Streaming rooms can either be joined as a streamer or as a viewer, depending on permissions.
  • Streaming rooms cannot be group rooms. They must have an associated spaceId and blockId.
  • To join a streaming room as a streamer, a user must have a certain space role or be allowed in (they knock first). Short of being allowed in, a user will be able to watch the live stream from the space where the room is.

Message

Chat and Post

Audio and Video

UI Overview

Navbar

Sidebar

Search

Command Palette

Organization Admin

Rooms Sidebar

Rooms Screen

Space

Home

Members

Description

Analytics

Full-Screen Blocks

Rich text

Rich text editing/rendering exists in the following contexts:

  • Messages
  • Various Block types, as rendered in the context of the space home page, space description, and as children of other blocks.
  • Block Responses (e.g. bold text in a comment)

This is complex, because open-source rich text editing frameworks are either native to iOS or Android and minimally maintained or are entirely contenteditable based, and work only with React DOM, not React Native. This has forced us to use web views on mobile for all rich text editing, therefore allowing the use of Lexical for all rich text editing use cases, combined with YJS to avoid conflicts when it comes to our blocks that include rich text.

Multiplayer experiences

Responsibility for the end-user experience of being in a 'multiplayer' application (seeing that other users are present beyond just seeing their changes to shared data) is distributed across multiple backend services:

  • Live cursors and cursor chat: Pilot/Dealer (websocket-based 'scopes' shared between multiple end-user clients)
  • Avatar stacks: Blockhead (via Friend, and (seperately) Dealer (visually merged by the client applications)
  • Presence indicators: Facebox (via Friend) for storage of last_seen_at and Dealer for collection and NATS-publication of presence pings. (Polling Facebook for a specific userId is required to get updates to other user's lastSeenAt values, unless the client in a Dealer scope or a space or block with them at the moment.)
  • Typing indicators and read receipts: Messenger (via Friend) for storage of read receipts and Pilot/Dealer (client websocket subscription) for client pub/sub of typing indicators and read receipts.

Custom Web Views

If an organization (on the enterprise tier) adds a user to a group and adds a customWebView to that group, they can trigger that user to see an item in the left sidebar with a custom name.

When a user clicks on such an item, we show an iFrame / webview pointing to that url. This is a useful feature when an organization has some third-party system that they want to cleanly embed in Pivot.

This functionality of course only works if the customIframeUrl's iFrame policy supports it.