Sharing
Share dashboards publicly with optional password protection
Dashboards can be shared publicly via unique token-based URLs. Shared dashboards support optional password protection, rate limiting, and read-only viewing with live query execution.
Public share URL format: /share/:token
Sharing Flow
Enabling Sharing
- Click the visibility dropdown in the dashboard toolbar (Private/Public toggle)
- A unique token is generated and stored
- The share URL becomes active immediately
Disabling Sharing
- Select "Private" from the visibility dropdown
- The share link is deleted
- Access is immediately revoked. Existing public URLs stop working.
Share Status
The shareToken and password status are tracked per-tab. The visibility dropdown in the toolbar reflects the current state (lock icon for private, globe icon for public).
Password Protection
Setting a Password
- When enabling sharing, an optional password can be provided
- Passwords are hashed with bcryptjs using 10 salt rounds
- A
nullhash means no password is required
Updating or Removing a Password
- You can update or remove the password on an existing share at any time
- Pass
nullto remove password protection
Password Verification Flow (Public Access)
- Public user visits
/share/:token - If password-protected and no password provided: a password gate UI is shown
- User submits password
- On mismatch: "Incorrect password" error (HTTP 401)
- On success: dashboard data is returned and rendered
The password is also required for query execution. The frontend stores it in a ref and sends it with each request.
Rate Limiting
Two rate limiters protect public sharing endpoints. Both use an in-memory store with automatic cleanup every 60 seconds.
Content Fetch (sharing.getPublic)
- Limit: 30 requests per 60 seconds
- Key: Client IP (via
x-forwarded-fororx-real-ipheader) - Returns HTTP 429 with
Retry-Afterheader when exceeded
Query Execution (sharing.executePublicQueries)
- Limit: 10 requests per 60 seconds
- Key:
{IP}:{token}(per-IP, per-dashboard) - Falls back to IP-only if token extraction fails
- Returns HTTP 429 with
Retry-Afterheader when exceeded
When rate limited, the frontend shows a dismissible banner: "Too many requests. Please wait a moment and try again."
Public Share Page
States
| State | UI |
|---|---|
| Loading | Centered spinner |
| Error | Error message with DB Pro branding |
| Password required | Password form |
| Dashboard loaded | Full-screen dashboard in read-only mode |
| Query error | Dismissible banner above the dashboard |
Load Sequence
- Extract
tokenfrom URL params - Fetch dashboard data using the token
- Handle response:
- Token not found: "This shared link is no longer available"
- Password required: show password gate
- Dashboard data returned: deserialize and render
- Automatically execute widget queries to fetch data
- Dashboard renders in read-only mode
Read-Only Mode
Shared dashboards render with readOnly enabled:
- Drag-and-drop is disabled
- Resize handles are hidden
- Context menus are hidden (no Edit/Duplicate/Remove)
- Manual refresh via the toolbar button is allowed (subject to rate limiting)
- Auto-refresh intervals are supported
Branding
A "Powered by DB Pro" badge is fixed to the bottom-right corner with a link to dbpro.app.
API Endpoints
Protected (Authenticated)
| Endpoint | Input | Output |
|---|---|---|
sharing.enable | { entityType, entityId, password? } | { token } |
sharing.disable | { entityType, entityId } | { success: true } |
sharing.updatePassword | { entityType, entityId, password | null } | { success: true } |
sharing.getStatus | { entityType, entityId } | { token, hasPassword } | null |
Public (Unauthenticated, Rate Limited)
| Endpoint | Input | Output |
|---|---|---|
sharing.getPublic | { token, password? } | { entityType, name, data } | { requiresPassword } | null |
sharing.executePublicQueries | { token, password? } | Record<number, { rows, fields }> |
Query Execution for Public Shares
When a public user views a shared dashboard:
- The server fetches the dashboard and its associated connection
- Connection credentials are decrypted (AES-256-GCM)
- Widget queries are extracted from the serialized dashboard data
- Queries execute in parallel via
Promise.allSettled - Results are mapped back to widget indices and returned
Public users can only execute the pre-configured widget queries. They cannot run arbitrary SQL.
Database Schema
shared_links Table
| Column | Type | Description |
|---|---|---|
id | TEXT (PK) | CUID2 auto-generated |
token | TEXT (unique) | Public share token (CUID2) |
entity_type | TEXT | "dashboard" or "saved_query" |
entity_id | TEXT | Reference to the shared entity |
password_hash | TEXT (nullable) | bcrypt hash, null = no password |
created_by | TEXT (FK) | References users.id |
created_at | TEXT | ISO 8601 timestamp |
Error Messages
| Scenario | Backend Code | User-Facing Message |
|---|---|---|
| Token not found | NOT_FOUND | "This shared link is no longer available" |
| Wrong password | UNAUTHORIZED | "Incorrect password" |
| No password provided (required) | UNAUTHORIZED | "Password required" |
| Rate limit exceeded | 429 | "Too many requests. Please wait a moment and try again." |
| Unsupported database | BAD_REQUEST | "Database type is not supported" |
| Dashboard not found | NOT_FOUND | "Dashboard not found" |
| Connection not found | NOT_FOUND | "Connection not found" |
| Generic failure | -- | "Failed to load shared content" |
Security
- Token generation: CUID2 (cryptographically secure random IDs)
- Password hashing: bcryptjs with 10 salt rounds
- Connection encryption: AES-256-GCM for stored credentials
- Rate limiting: Dual-layer (IP-based for content fetch, IP+token for query execution)
- Query scope: Public users can only execute pre-configured widget queries, not arbitrary SQL
- Immediate revocation: Disabling sharing deletes the token row, instantly invalidating all public access