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
typeenum value. This includes validation forBlockData,BlockResponse,BlockViewand 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
typeenum 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, andBlockResponserecords. -
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
SubscribableFeaturepotentially.
Block Types Google Sheet (opens in a new tab)
The feature set provided by blocks involves more than just the
Blockentity. 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:
- The new parent does not support children of the block type that the block being moved is
- AND there is not an obvious block type change that can be inferred.
Inferred block type changes are:
- Database Item -> Page (and reverse)
- Goal -> Page (and reverse)
- 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:
- Updates to the
richTextContentfield can be made with thebinaryBlockUpdatemutation/method. - 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
POLLblock 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
spaceIdandblockId. - 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_atand Dealer for collection and NATS-publication of presence pings. (Polling Facebook for a specific userId is required to get updates to other user'slastSeenAtvalues, 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.