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:
| Function | Description |
|---|---|
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
| File | Route | Purpose |
|---|---|---|
DataRoomsPage.tsx | /rooms | Fluid header + responsive grid (1-3 cols). Usage dots + upgrade banner |
DataRoomDetail.tsx | /rooms/:roomId | Cleaned stats grid (4-col), prioritized "Copy Link" action. |
ManageDataRoom.tsx | /rooms/new, /rooms/:roomId/edit | Create / edit room form |
DataRoomViewer.tsx | /:handle/:slug | Public viewer with access gate + sidebar navigation |
Components
| Component | Location | Purpose |
|---|---|---|
DataRoomCard | components/dashboard/ | Card with grid pattern + corner glow texture |
DocumentPicker | components/dashboard/ | Modal for selecting existing decks |
RoomDocumentList | components/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:
| Tier | Max Data Rooms | Enforcement | Max Decks Per Room |
|---|---|---|---|
| FREE | 1 | Button disabled + upgrade banner | 50 |
| PRO | 5 | Button disabled + upgrade banner | 500 |
| PRO+ | ∞ (Unlimited) | No limit | ∞ (Unlimited) |
Where limits are enforced:
DataRoomsPage.tsx— Primary UI enforcement. Shows usage dots, locks "New Room" button, displays contextual upgrade banner.ManageDataRoom.tsx— Safety net. Checks room count on mount in create mode; redirects to/roomsif at limit.- 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
COALESCEguards to ensure metadata processing doesn't block the initial icon upload. - Signing: Although icons are in the
publicbucket 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
- User clicks "New Room" on
/rooms(if under tier limit) - 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. - Optionally sets access controls (email gate, password, or expiration).
- Saves → redirected to room detail page. Any validation errors (e.g. slug already taken) are surfaced via non-blocking
sonnernotifications.
Security Gates & Private Storage
- Email Gate: Standard regex-based email validation.
- Password Gate: Server-side verification via
check_data_room_passwordRPC. - Expiration: Enforced at the database layer (Postgres). Once a room's
expires_attimestamp is passed, the specializedget_data_room_payloadRPC will fail to resolve the asset, ensuring immediate revocation across all sessions. - Private Storage Path: The room payload includes a
storage_pathfor 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
DocumentPickermodal 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
DataRoomViewerwith 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"
Navigation After Editing
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)
