Skip to content

Data Rooms

Data Rooms let users bundle multiple documents into a single shareable folder with its own access controls, analytics, and branding.


Architecture Overview

Database Schema

text
data_rooms
├── id (uuid, PK)
├── user_id (uuid, FK → auth.users)
├── name (text)
├── slug (text, unique)
├── description (text, nullable)
├── icon_url (text, nullable)
├── require_email (boolean, default false)
├── require_password (boolean, default false)
├── view_password (text, nullable)
├── created_at (timestamptz)
└── updated_at (timestamptz)

data_room_documents (junction table)
├── id (uuid, PK)
├── data_room_id (uuid, FK → data_rooms)
├── deck_id (uuid, FK → decks)
├── display_order (integer)
└── added_at (timestamptz)

Service Layer — dataRoomService.ts

Core functions:

FunctionDescription
getDataRooms()Fetch all rooms for current user (cached)
getDataRoom(id)Fetch single room by ID
createDataRoom(data)Create new room, returns room record
updateDataRoom(id, data)Update room metadata
deleteDataRoom(id)Delete room + remove junction entries
getDocuments(roomId)Fetch all decks linked to a room (ordered)
addDocuments(roomId, deckIds)Link existing decks to a room
removeDocument(roomId, deckId)Unlink a deck from a room
reorderDocuments(roomId, deckIds)Update display_order for all docs
getDocumentCount(roomId)Count of linked documents
getDataRoomAnalytics(roomId)Aggregate visitor analytics across all docs
uploadRoomIcon(file)Upload custom icon to assets bucket

Key Pages & Components

Pages

FileRoutePurpose
DataRoomsPage.tsx/roomsFluid header + responsive grid (1-3 cols). Usage dots + upgrade banner
DataRoomDetail.tsx/rooms/:roomIdCleaned stats grid (4-col), prioritized "Copy Link" action.
ManageDataRoom.tsx/rooms/new, /rooms/:roomId/editCreate / edit room form
DataRoomViewer.tsx/:handle/:slugPublic viewer with access gate + sidebar navigation

Components

ComponentLocationPurpose
DataRoomCardcomponents/dashboard/Card with grid pattern + corner glow texture
DocumentPickercomponents/dashboard/Modal for selecting existing decks
RoomDocumentListcomponents/dashboard/Drag-reorderable list with optimistic performance

Tier Limits

Data room creation is gated by the user's subscription tier, managed centrally in constants/tiers.ts:

TierMax Data RoomsEnforcementMax Decks Per Room
FREE1Button disabled + upgrade banner50
PRO5Button disabled + upgrade banner500
PRO+∞ (Unlimited)No limit∞ (Unlimited)

Where limits are enforced:

  1. DataRoomsPage.tsx — Primary UI enforcement. Shows usage dots, locks "New Room" button, displays contextual upgrade banner.
  2. ManageDataRoom.tsx — Safety net. Checks room count on mount in create mode; redirects to /rooms if at limit.
  3. Database Trigger (tr_limit_decks_per_room) — Enforces the maximum number of decks allowed inside a single room based on the owner's tier, ensuring platform stability.

Custom Branding (Icons)

Data rooms support custom icon uploads to the assets storage bucket.

  • Pathing: assets/{userId}/room-icons/icon-{timestamp}.{ext}.
  • Hardening: The bucket RLS uses COALESCE guards to ensure metadata processing doesn't block the initial icon upload.
  • Signing: Although icons are in the public bucket for fast viewer loading, the owner's dashboard uses $O(N)$ Map-based lookups for document thumbnails to maintain high performance.

Changing limits

Edit the maxDataRooms value in TIER_CONFIG inside constants/tiers.ts:

ts
export const TIER_CONFIG: Record<Tier, TierConfig> = {
  FREE: { days: 7, label: "7 Day Analytics", maxDataRooms: 1 },
  PRO: { days: 90, label: "90 Day Analytics", maxDataRooms: 5 },
  PRO_PLUS: { days: 365, label: "1 Year Analytics", maxDataRooms: Infinity },
};

User Flows

Creating a Room

  1. User clicks "New Room" on /rooms (if under tier limit)
  2. Fills in name, URL slug, description on ManageDataRoom. Slugs are name-spaced under the user's handle (e.g. deckly.app/alice/seed-round). Note that slugs must be globally unique across the platform to ensure secure link generation.
  3. Optionally sets access controls (email gate, password, or expiration).
  4. Saves → redirected to room detail page. Any validation errors (e.g. slug already taken) are surfaced via non-blocking sonner notifications.

Security Gates & Private Storage

  • Email Gate: Standard regex-based email validation.
  • Password Gate: Server-side verification via check_data_room_password RPC.
  • Expiration: Enforced at the database layer (Postgres). Once a room's expires_at timestamp is passed, the specialized get_data_room_payload RPC will fail to resolve the asset, ensuring immediate revocation across all sessions.
  • Private Storage Path: The room payload includes a storage_path for each document. This path is used by the client to request a short-lived signed URL, ensuring that document access remains gated behind the room's security logic.

Adding Documents to a Room

Two paths from the room detail page:

  • "Add Existing" — Opens DocumentPicker modal to select from uploaded decks
  • "Upload New" — Navigates to /upload?returnToRoom=<roomId>. After upload completes, the deck is auto-added and user redirects back to the room

Sharing a Room

  • Copy the public link (/:handle/:slug) from the room detail page
  • Visitors see the DataRoomViewer with either a desktop sidebar or mobile drawer navigation between documents
  • Access gates (email/password) are applied before document viewing begins. The AccessGate ensures data integrity through robust regex-based email validation and automatic input trimming.
  • Visitor engagement is aggregated across all documents inside the room onto the Room's detail page via "Visitor Signals"

When editing a room via /rooms/:roomId/edit:

  • Save → returns to /rooms/:roomId (detail page)
  • Delete → returns to /rooms (listing)
  • Back button → returns to /rooms/:roomId (detail page)

Built with ❤️ for Founders