Skip to main content

Overview

Private rooms in Plant Together provide secure, controlled access to your diagrams. Only the room owner and explicitly invited participants can access private rooms, making them ideal for sensitive work, client projects, and team collaboration.

Private vs Public Rooms

Characteristics:
  • Open to anyone with the room name
  • URL: https://planttogether.com/#/{roomName}
  • No access control
  • Cannot have duplicate names globally
Best for:
  • Open source projects
  • Public documentation
  • Community collaboration
  • Educational content
Public rooms are great for transparency but offer no privacy protection.

Creating Private Rooms

1

Toggle Privacy Setting

On the landing page, enable the Private toggle switch before submitting your room name.
// Source: landing.page.tsx:15
const [isPrivate, setIsPrivate] = useState(false)
2

Submit Room Name

Enter your room name and click Submit. The room will be created with private access control.
// Source: landing.page.tsx:38-40
const roomToGoTo = isPrivate
  ? `private/${userContext?.context?.userId}/${roomName}`
  : roomName
Private rooms are namespaced by user ID, so you can create my-project even if another user has a public or private room named my-project.
3

Access Your Private Room

You’ll be redirected to your private room:
/#/private/user_abc123/project-diagrams
          └─────┬────┘ └────────┬────────┘
           Owner ID       Room Name

Access Control System

How Private Access Works

Private rooms use a participant list stored in the database. Only users on this list can access the room.
1

Room Creation

When you create a private room, you’re automatically added as the owner and participant.
// Source: room.service.ts:54-62
const ownerId = this.authService.getUserId(token!)
if (room.owner_id !== ownerId) {
  return false
}
2

Access Validation

When someone tries to access the room, the server checks:
// Source: room.service.ts:79-86
async validateUserPrivateAccess(token: string, roomId: string) {
  try {
    const userId = this.authService.getUserId(token)
    return await this.roomParticipantRepo.userPrivateAccess(
      roomId, 
      userId
    )
  } catch (error: any) {
    throw new Error('Failed to validate user access to room')
  }
}
3

Access Granted or Denied

  • Granted: User is owner or on participant list
  • Denied: User receives “Room not found” error
The system intentionally shows “Room not found” instead of “Access denied” to prevent room name enumeration attacks.

Sharing Private Rooms

Signature-Based Invitations

Private rooms use cryptographically signed URLs to grant access:
1

Generate Share Link

As the room owner, click the Share Room button in the navigation bar.
// Source: navBar.component.tsx:49-61
const handleShare = async () => {
  try {
    const { signature } = await plantService.shareRoom(roomId)
    const shareUrl = `${window.location.origin}/#/private/${ownerId}/${roomName}?signature=${signature}`

    // Copy to clipboard
    await navigator.clipboard.writeText(shareUrl)
    alert('Share link copied to clipboard!')
  } catch (error: any) {
    console.error('Failed to share room:', error)
  }
}
2

Server Creates Signature

The server signs the room ID using a secret key:
// Source: room.service.ts:89-96
async createRoomSignature(roomId: string) {
  try {
    if (!roomId) throw new Error('Room not found or not accessible')
    return this.signature.sign(roomId)
  } catch (error: any) {
    throw new Error('Failed to sign room url')
  }
}
The signature is generated using the signed package:
// Source: room.service.ts:6-27
import { Signature, SignatureOptions } from 'signed'
import { ROOM_SIGNATURE_SECRET } from '../config.js'

constructor(/* ... */) {
  this.signature = signed({ secret: ROOM_SIGNATURE_SECRET! })
}
3

Share URL Contains Signature

The generated URL looks like:
https://planttogether.com/#/private/user123/design-docs?signature=eyJhbGc...
Components:
  • user123 - Owner’s user ID
  • design-docs - Room name
  • signature=eyJhbGc... - Cryptographic signature
4

Recipient Opens Link

When someone clicks the share link:
// Source: collabRoom.page.tsx:22-23
const [searchParams] = useSearchParams()
const signature = searchParams.get('signature')
The signature is extracted from the URL query parameters.
5

Server Validates Signature

The server verifies the signature and grants access:
// Source: room.service.ts:99-113
async processRoomSignature(
  token: string, 
  roomId: string, 
  signature: string
) {
  try {
    if (!roomId) throw new Error('No room provided')

    const userId = this.authService.getUserId(token)
    const verifiedRoomId = this.signature.verify(signature)

    if (verifiedRoomId !== roomId)
      throw new Error("Signature doesn't match requested room")

    // Add user to participant list
    this.roomParticipantRepo.addUserAccess(verifiedRoomId, userId)
  } catch (error: any) {
    throw new Error('Failed to sign room url')
  }
}
6

Access Granted Permanently

Once validated, the user is added to the participant list and can access the room anytime without needing the signature again.
Signatures are single-use for access granting, but the access itself is permanent until revoked by the owner.

Security Properties

Tamper-Proof

Signatures are cryptographically signed. Any modification to the URL invalidates the signature.
✅ Valid:
?signature=abc123...

❌ Invalid (tampered):
?signature=abc124...

Server-Side Validation

All validation happens on the server. Clients cannot bypass access controls.
// Cannot be forged client-side
const verifiedRoomId = this.signature.verify(signature)

Secret Key Protection

Signatures are created using a server-side secret:
// Source: room.service.ts:27
this.signature = signed({ 
  secret: ROOM_SIGNATURE_SECRET! 
})
The secret never leaves the server.

Room-Specific

Each signature is tied to a specific room ID:
if (verifiedRoomId !== roomId)
  throw new Error("Signature doesn't match")
You can’t reuse a signature for different rooms.

Toggling Room Privacy

Only room owners who are authenticated users (not guests) can toggle room privacy.

Switching from Public to Private

1

Click the Privacy Button

In the navigation bar, click the Public Room button (green with unlocked icon).
// Source: navBar.component.tsx:80-96
{!userContext.context.isGuest && isOwner && (
  <Button
    size={ButtonSize.sm}
    onClick={handleAccessToggle}
    disabled={isLoading}
    className={`${isPrivate ? 'bg-blue-500' : 'bg-green-500'}`}
  >
    <FontAwesomeIcon
      icon={isPrivate ? faLock : faLockOpen}
    />
    {isLoading
      ? 'Changing...'
      : isPrivate ? 'Private Room' : 'Public Room'
    }
  </Button>
)}
2

Server Updates Access

The server changes the room’s privacy setting:
// Source: plant.service.tsx:273-292
export const changeRoomAccess = async (
  roomId: string, 
  isPrivate: boolean
) => {
  const token = await retrieveToken()
  const response = await fetch(
    `${serverHttpUrl}/room/${roomId}/access`,
    {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        Authorization: token,
      },
      body: JSON.stringify({ is_private: isPrivate }),
    }
  )
  if (!response.ok) throw new Error('Failed to change access')
}
// Backend - Source: room.service.ts:116-123
async changeRoomAccess(room_id: string, isPrivate: boolean) {
  try {
    await this.roomRepo.updateRoomAccess(room_id, isPrivate)
  } catch (error) {
    throw new Error('Error updating room access')
  }
}
3

URL Changes

You’ll be redirected to the new URL:
// Source: navBar.component.tsx:35-40
const navigateLink = !isPrivate
  ? `/private/${userContext.context.userId}/${roomName}`
  : `/${roomName}`

await plantService.changeRoomAccess(roomId, !isPrivate)
navigate(navigateLink)
Before (public):
/#/my-project
After (private):
/#/private/user123/my-project

Switching from Private to Public

The same process works in reverse:
  1. Click the Private Room button (blue with locked icon)
  2. Room becomes publicly accessible
  3. URL changes from /#/private/{userId}/{roomName} to /#/{roomName}
When switching to public, the participant list is retained but not enforced. Anyone can now access the room by name.

Room Ownership and Permissions

Owner Capabilities

Room owners can:
  • ✅ Toggle room privacy
  • ✅ Share invitation links
  • ✅ Create, rename, and delete documents
  • ✅ Edit all content
  • ✅ Export diagrams
  • ✅ Invite unlimited participants

Participant Capabilities

Invited participants can:
  • ✅ View and edit all documents in real-time
  • ✅ Create new documents
  • ✅ Rename and delete documents
  • ✅ Export diagrams
  • ❌ Change room privacy
  • ❌ Generate share links (owner only)
All participants have equal editing rights. Plant Together does not currently support read-only or limited-edit access.

Room Discovery and Access

Accessing Your Private Rooms

If you know the room name, type it on the landing page with Private mode enabled:
Room name: my-project
Private: ✅ Enabled
You’ll be taken to:
/#/private/{your-user-id}/my-project
Bookmark your frequently used private rooms:
📑 Bookmarks
  └─ Work Project: /#/private/user123/work-arch
  └─ Client Designs: /#/private/user123/client-designs
  └─ Team Planning: /#/private/user123/sprint-diagrams

Room Not Found Errors

If you see “Room not found” when accessing a private room:
1

Verify the URL

Check for typos in the room name or owner ID:
❌ /#/private/user12/my-project  (wrong owner ID)
✅ /#/private/user123/my-project
2

Confirm Access

You might not have been granted access:
  • Request a share link from the room owner
  • Verify you’re logged into the correct account
3

Check Room Existence

The room may have been deleted or never created:
  • Contact the room owner
  • Verify the link wasn’t corrupted

Best Practices

Use Private for Sensitive Work

Always use private rooms for:
  • Client projects
  • Internal architecture
  • Proprietary designs
  • Security-sensitive diagrams

Share Links Securely

Treat share links like passwords:
  • ✅ Send via secure channels (Slack DM, email)
  • ❌ Don’t post in public channels
  • ❌ Don’t share on social media
  • ✅ Verify recipient identity

Organize Room Names

Use clear naming conventions:
✅ client-acme-api-design
✅ internal-payment-flow
✅ team-alpha-sprint-5

❌ room1
❌ test
❌ asdf

Regular Access Audits

Periodically review who has access:
  • Consider creating new rooms for sensitive projects
  • Remove access by creating a new room (no revoke feature yet)
  • Keep track of who you’ve shared links with

Limitations and Considerations

Current limitation: Once someone has access, you cannot revoke it.Workaround:
  1. Create a new private room
  2. Copy content from old room
  3. Share new room only with trusted participants
  4. Abandon the old room
Limitation: Guest users’ private rooms are session-based.Impact:
  • Private rooms disappear when session ends
  • Cannot share with signature links effectively
  • Not recommended for persistent work
Solution: Create a free account for persistent private rooms.
Limitation: Plant Together doesn’t provide a “my rooms” page.Impact:
  • You must remember or bookmark your room names
  • No centralized view of all your rooms
Workaround: Maintain a personal list or bookmarks.

Troubleshooting

Cause: You’re not viewing a private room or you’re not the owner.Solution:
  • Verify the URL starts with /#/private/
  • Confirm you’re logged in as the room owner
  • Guest users cannot share private rooms
Possible causes:
  • You’re not the room owner
  • You’re logged in as a guest
  • Room is being used by others
Solution:
  • Only the authenticated owner can toggle privacy
  • Create a free account if you’re a guest
Cause: The signature doesn’t match the room ID.Solutions:
  • Regenerate the share link
  • Verify no characters were dropped when copying
  • Check for URL encoding issues

Next Steps

Create Your First Room

Learn the full room creation process

Collaborate in Real-Time

Understand how multiple users work together

Export Private Diagrams

Download your secure diagrams

API Reference

Integrate room sharing into your workflows

Build docs developers (and LLMs) love