Deckly Architecture
This is a living document describing the technical "How" of Deckly.
System Overview
The app is built as a Heavy-Client SPA leveraging Supabase for high-speed data persistence.
graph TD
A[PDF Upload] --> B[Browser-side Processing]
B -->|pdfjs-dist| C[Canvas Rendering]
C -->|WebP Compression| D[Supabase Storage]
D --> E[PostgreSQL Record]
E --> F[Dashboard / Viewer]
F -->|Auth Gate| P[get_deck_payload RPC]
P -->|Private Path| S[sign-deck-url Edge Function]
S -->|Signed URL| F
F -->|Friendly URL| J[/:handle/:slug]
J --> F
subgraph "Data & State"
F --> K[TanStack Query Cache]
K --> L[Supabase / Service API]
F -->|Optimistic UI| M[Investor Notes / Saves]
end
subgraph Analytics
F --> G[Heartbeat Tracker]
G -->|FETCH| V[Vercel Edge API /api/geo]
V -->|x-vercel-ip-country/city| G
G --> H[record_deck_visit RPC]
end
Core Components
| Layer | Technology | Responsibility |
|---|---|---|
| Frontend | React + Vite | UI/UX & Code-Splitting (Lazy Load) |
| Auth | Supabase Auth | Gated access & Session management |
| Branding | TanStack Query | Centralized profile & mission-control state |
| Storage | Supabase Buckets | Large asset (PDF/Images) hosting |
| Compute | Browser | PDF rendering (reduces server costs) |
| State | TanStack Query | Async caching & optimistic updates |
| Analytics | Custom Hook | Real-time 45s heartbeat refresh (useQuery) |
| Geo-Location | Vercel Edge API | IP-based country/city extraction via headers |
| Optimization | Postgres RPCs | Offloaded heavy GROUP BY and DISTINCT logic to DB |
| Performance | Layered Prefetch | Tiered prefetching & lazy-module preloading |
| Utilities | url.ts | Centralized share-link and internal routing |
Layered Performance Architecture
To maintain a "zero-latency" feel across the platform, Deckly uses a three-tier performance strategy orchestrated globally in Home.tsx and contextually in navigation components.
1. Layer 1: Global Readiness (Boot-Time)
Upon landing on the dashboard, the application identifies idle time to prepare the environment:
- Data Priming: Prefetches high-level metadata lists (Decks, Rooms, Saved Decks) via TanStack Query.
- Module Warming: Pre-imports the JS chunks for heavy routes (e.g.
Viewer.tsx,DataRoomsPage.tsx) using backgroundimport()calls. This removes "loading..." flickers during navigation.
2. Layer 2: Intent-Based Prefetching (Hover)
We anticipate user navigation by tracking cursor movement:
- Sidebar Hover: Hovering over a sidebar link triggers a pre-import of that page's specific chunk and its primary data query.
- Card Hover: Hovering over a
DataRoomCardorDocumentRowprefetches its specific metadata and document list ~100-300ms before the click.
3. Layer 3: Deferred Heavy Operations
To preserve database resources and bandwidth:
- Analytics Deferral: Deep analytics (individual visitor trends, historical charts) are never prefetched. They load strictly on-demand when the user navigates into a detail view.
- Metadata Bundling: Prefetching is limited to lightweight metadata (titles, counts) and omits heavy image thumbnails or binary assets.
UI Component Standards
Deckly follows a strict premium UI standard utilizing Framer Motion for interactivity and Tailwind CSS for obsidian-themed styling.
- Obsidian Depth: Core backgrounds and sidebars use
#10120ffor grounded, professional contrast. - Typography: Standardized on sentence-case (not uppercase) for a more human, readable interface.
- Glassmorphism: Subtle patterns, grid textures, and
backdrop-blurfor high-end cards.
Security Model
- RLS (Row Level Security): Strict
auth.uid()checks on all sensitive rows. Write access to analytics is strictly routed throughrecord_deck_visit(Security Definer RPC), preventing unauthorized data injection. - Granular Storage Policies: Storage security is enforced via operation-specific policies (
INSERT,UPDATE,DELETE,SELECT) rather than monolithicFOR ALLblocks. This ensures thatWITH CHECKconstraints (like size limits) are correctly and exclusively applied during write operations. - Transactional Advisory Locks: We use
pg_advisory_xact_lock(hashtext(v_ip))to serialize sensitive concurrent operations, such as signup throttling. This eliminates TOCTOC (Time-of-Check to Time-of-Use) race conditions by ensuring a single IP can only execute the check+insert sequence one transaction at a time. - Modern API Access Control: Uses namespaced
VITE_SUPABASE_PUBLISHABLE_KEYandPROJECT_SECRET_KEYarchitecture. - Asymmetric JWT Verification: Authentication tokens are verified using RSA Public Keys as part of the project's hardened security infrastructure.
- Private Storage: The
decksstorage bucket is set toprivate. Direct URL access is blocked; all asset retrieval requires a short-lived signed URL generated via a secure Edge Function. - Robust IP Rate Limiting: Security RPCs correctly parse the
x-forwarded-forheader to identify unique client IPs, preventing shared 'unknown' buckets and ensuring accurate brute-force protection. - Slug Enumeration Prevention: The
get_payloadRPCs for decks and rooms unify success/failure responses. Both non-existent slugs and incorrect passwords return a generic 'Unauthorized' error, preventing attackers from discovering valid URLs. - Security Invoker Views: All public-facing views (
profiles_public) usesecurity_invoker=trueto prevent unauthorized data exposure through view-based logic. - Gated Links: Multi-layer protection (Email/Password) verified server-side via the hardened
get_deck_payloadandcheck_deck_passwordRPCs. AccessGate now uses robust regex-based email validation and input trimming to ensure data integrity. - RLS Resiliency: Storage policies (especially on the
assetsbucket) utilizeCOALESCEguards for size checks:COALESCE((metadata->>'size')::bigint, 0) <= limits. This prevents upload failures when metadata (populated asynchronously by Supabase) is missing during the initial request phase. - PII-Aware Logging: Edge Functions (
sign-deck-url, etc.) implement automatic redaction of sensitive identifiers, including User UUIDs and internal storage paths, before logging to protect user privacy in observability logs. - XSS & Protocol Sanitization: Slide hotspots and external links are strictly validated to permit only secure protocols (
https:,mailto:,tel:) and enforced withrel="noopener noreferrer"to prevent tab-nabbing.
Private Storage & Signed URLs
To prevent unauthorized file scraping and bypass of platform-level security (like passwords and expiry), Deckly implements a secure signing orchestration.
1. The Signing Flow
- Payload Resolution: The client calls
get_deck_payloadorget_data_room_payload. These RPCs return astorage_pathfor each page/document only if all security checks (password, expiry) pass. - URL Signing: The client sends the
storage_pathto thesign-deck-urlSupabase Edge Function. - Verification: The Edge Function re-validates access permissions (re-calling the database) and verifies the requested
storage_pathagainst the canonical path returned by the database (preventing IDOR). - Token Issuance: Upon success, the Edge Function returns a short-lived (1-hour) signed URL and its expiry metadata.
2. Auto-Refresh Mechanism
The Viewer.tsx component monitors the expires_in metadata. To ensure zero-latency viewing during long sessions, it initiates a background refresh ~60 seconds before the signed URL expires, re-authenticating and updating the asset pointers without interrupting the user.
Smart Slide Architecture
Deckly uses a dual-path processing pipeline to ensure fast, beautiful documents while minimizing server load. This is known as "Smart Slide" rendering (Image + Tracking Layer).
1. Client-Side (PDFs)
PDFs are processed entirely in the user's browser using pdfjs-dist.
- Rasterization: Pages are rendered to
<canvas>and converted to WebP blobs for optimized storage. - Link Extraction: The
extractPdfLinkHotspots()utility scans the PDF annotations to find hyperlinks. - Normalization: Coordinates are normalized to Percentages (0-1). This ensures that the interactive hotspots scale perfectly with the image, regardless of the viewer's screen resolution.
2. Server-Side (Office/Other)
Files like .pptx, .docx, and .xlsx are processed via a Supabase Edge Function (document-processor).
- Conversion: Uses ConvertAPI to transform multi-page documents into high-resolution JPG images.
- Direct Transformation: These images are uploaded to the
decksbucket in standard document order.
3. The Interactive Viewer
The ImageDeckViewer.tsx component is the high-fidelity presentation layer. It renders "Smart Slides" by:
- Displaying the rasterized image (WebP/JPG).
- Layering an invisible, absolute-positioned grid of
<a>tags (hotspots) on top based on theSlidePage.linksarray.
Pipeline Resilience
1. Atomic Rollback Architecture
To prevent orphaned assets and broken deck records, the conversion pipeline now lives in useManageDeckWorkflow.ts and implements a robust cleanup mechanism.
- DB-State Awareness: Instead of relying on potentially stale local component variables (which can be reset in interactive mode), the rollback logic queries the live database state immediately before performing a cleanup.
- Deep Comparison: Reclaimed storage paths are computed by comparing the current DB
pagesarray against thepreviousValues.pagessnapshot. - Purge Process: Any assets present in the database that are NOT part of the intended rollback state are purged from Supabase Storage, ensuring zero-waste asset management.
2. Batch Analytics Optimization
The Data Rooms dashboard avoids "N+1" query patterns by using the get_batch_data_room_analytics RPC.
- Single-Trip Retrieval: A single PostgreSQL call returns document counts and unique visitor counts for an entire list of room UUIDs.
- Set-Based Aggregation: Aggregates unique
visitor_ids fromdeck_page_viewscross-referenced withdata_room_documentsin the database layer, delivering O(1) performance to the frontend.
3. Type-Safe Analytics Pipeline
Internal dashboard analytics (Decks/Rooms) utilize the DeckPageStats interface for aggregation.
- Explicit Typing: Replaced legacy
anytypes with a strict interface for per-page metrics (views, duration, retention). - Service Integration: The
analyticsService.getDeckStatsaggregator ensures every data point is mapped to a fullDeckPageStatsobject, preventing "incomplete object" issues common in manual JSreducebuilding.
4. Robust Path Extraction
To support the private storage model, the database and Edge Functions use regex-based path normalization instead of brittle string splitting.
- Regexp Pattern:
\/storage\/v1\/object\/(?:public|sign|authenticated)\/decks\/(.+). - Universality: This allows utility functions like
get_owner_thumbnailsand thesign-deck-urlfunction to correctly extract the base storage path regardless of whether the incoming URL is a public link, a signed link, or an authenticated bucket link.
3. Current Refactor Direction
The repo has recently moved toward clearer boundaries without a big-bang rewrite:
ManageDeck.tsxis now a composition layeruseManageDeckWorkflow.tsowns the heavy upload/edit workflowsrc/components/dashboard/manage-deck/ManageDeckSections.tsxholds the screen sectionssrc/workflows/deckProcessing.tsowns shared PDF rendering/image generationdeckService.tsis now a facade over narrower service modules- auth/session resolution is being standardized through shared helpers instead of repeated ad-hoc lookups
Admin & Notifications
1. Secure Admin Access
Access to administrative routes (e.g., /admin) is strictly gated by the is_admin() PostgreSQL function.
- Server-Side Verification: The logic checks the current
auth.uid()against thepublic.admin_emailstable. - RPC Gating: The frontend performs a mandatory async check on mount, preventing access even if client-side variables are tampered with.
2. Notification Persistence & Cleanup
- Batched Deletion: To prevent long-held table locks during cleanup, the
cleanup_expired_notifications()job uses aLOOPwith aLIMIT 1000batch size. This ensures high availability for thenotificationstable even during large-scale data maintenance. - Deduplication Performance: A partial index
idx_notifications_dedupon(user_id, type, title, created_at DESC)filtered byread_at IS NULLensures that deduplication checks are near-instant.
3. Visitor Counting Accuracy
The notify_signal_threshold trigger ensures that decks.unique_visitors only increments for first-time visits by a specific visitor_id.
- EXISTS Guard: Since the
deck_page_viewstable legitimately allows multiple rows per visitor (one per page viewed), the trigger uses anEXISTScheck to avoid overcounting return visits or multi-page sessions. - Idempotency: This approach maintains a high-performance, atomic source of truth for investor interest metrics.
