Skip to content

Data Rooms

Data Rooms let users bundle multiple documents into a single shareable room with its own access controls, analytics, branding, flat folders, owner-only tagging through the shared global tag model, and private room notes.


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)
├── folder_id (uuid, FK → data_room_folders, nullable)
├── display_order (integer)
└── added_at (timestamptz)

data_room_folders
├── id (uuid, PK)
├── data_room_id (uuid, FK → data_rooms)
├── name (text)
├── color (text)
├── position (text)
├── created_by (uuid)
├── updated_by (uuid, nullable)
├── created_at (timestamptz)
└── updated_at (timestamptz)

global_tags
├── id (uuid, PK)
├── user_id (uuid, FK → auth.users)
├── name (text)
├── color (text)
├── created_at (timestamptz)
├── updated_at (timestamptz)
└── deleted_at (timestamptz, nullable)

global_tag_aliases
├── id (uuid, PK)
├── user_id (uuid, FK → auth.users)
├── alias_type (text: legacy_name | legacy_id)
├── alias_value (text)
├── tag_id (uuid, FK → global_tags)
├── created_at (timestamptz)
└── updated_at (timestamptz)

data_room_folder_tags (junction table)
├── folder_id (uuid, FK → data_room_folders)
└── tag_id (uuid, FK → global_tags)

data_room_document_tags (junction table)
├── document_id (uuid, FK → data_room_documents)
└── tag_id (uuid, FK → global_tags)

saved_data_rooms
├── id (uuid, PK)
├── user_id (uuid, FK → auth.users)
├── data_room_id (uuid, FK → data_rooms)
├── folder_id (uuid, FK → library_folders, nullable)
├── saved_at (timestamptz)
├── room_title (text snapshot)
├── room_slug (text snapshot)
└── is_deleted (boolean snapshot)

room_notes
├── user_id (uuid, FK → auth.users)
├── data_room_id (uuid, FK → data_rooms)
├── content (text)
└── updated_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 full linked room documents (ordered)
getDocumentSearchSummaries(roomId)Fetch lightweight search metadata for overview filtering
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
setDocumentTags(documentId, tagIds)Apply owner-only tags to one document
saveToLibrary(dataRoomId)Save a room into the user-private library
roomNoteService.saveNote(dataRoomId, content)Persist a private room note

Folder / Tag Rules

  • Flat grouping only: folders are top-level groups only. There is no nesting.
  • Permanent delete: folder deletion is hard delete. Documents inside return to Unorganized via folder_id = NULL.
  • Shared global tag source: folder and document tags now point at global_tags, so the same canonical user tag can be reused across the library, decks, saved rooms, folders, and room documents.
  • Owner-only folder tags: folder tags are visible only to the room owner and are not exposed in shared links.
  • Document tags: documents can also have their own owner-only global tags, rendered in the room detail view.
  • Alias compatibility: migrated room-local tags are preserved through global_tag_aliases so old IDs and names can still resolve during the transition.
  • Folder tag limit: each folder can have up to 4 tags.
  • Free-tier guard: Free users can create up to 1 folder per room; Pro downgrade gets a 15-day warning window before room access is blocked.
  • Saved-library separation: Saved room folders and tags are private library organization only; they do not change the room owner's original room folders or tags.
  • Tombstones: If a saved room is deleted, the saved-library entry stays visible as a tombstone so users can still unsave it or see the preserved note snapshot.

Key Pages & Components

Pages

FileRoutePurpose
DataRoomsPage.tsx/roomsFluid header + responsive grid (1-3 cols). Usage dots + upgrade banner
DataRoomDetail.tsx/rooms/:roomIdTabbed room shell with content, analytics, and settings panels.
ManageDataRoom.tsx/rooms/new, /rooms/:roomId/editCreate / edit room form
DataRoomViewer.tsx/:handle/:slugPublic viewer with access gate + folder-aware sidebar navigation
OwnerDataRoomPreview.tsx/preview/room/:roomIdOwner preview with signed URLs and the same deck rendering flow
SavedDecks.tsx/savedMixed saved library for decks and rooms with private notes

Components

ComponentLocationPurpose
DataRoomCardcomponents/dashboard/Card with grid pattern + corner glow texture
DocumentPickercomponents/dashboard/Modal for selecting existing decks
RoomDocumentListcomponents/dashboard/Drag-reorderable list with row actions, tags, and folder moves

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:

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

Working With Folders

  • Click a folder card to filter the room list to that folder.
  • Click the same folder again to return to the full room list.
  • Moving a document into a folder updates the room detail page and preview so the document appears inside that folder group.
  • The room preview uses signed URLs for both the owner preview route and the public shared route, so local preview and incognito/shared viewing render the same assets.
  • DataRoomsPage.tsx no longer loads every full room document just to power search and tag filters.
  • The overview now requests lightweight per-room document summaries through dataRoomService.getDocumentSearchSummaries(roomId).
  • Full document payloads remain on-demand in room detail and viewer flows.

Document Actions

  • Each row has a 3-dot menu for analytics, edit, and removal from the room.
  • The tag button on a document applies owner-only document tags and renders the chips inline in the row.

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"
  • Shared links show folder grouping, but never expose owner-only document or folder tags.
  • Saving a room from the viewer or owner preview writes a private saved-library row for the current user.
  • Private notes are room-scoped, stay hidden from shared links, and survive unsave so they can return on later resave.
  • Deleted rooms remain manageable in the saved library as tombstones instead of silently disappearing.

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