Skip to main content

Shared report links

What this is​

Shared report links let someone view a read-only report without signing in. Each link is limited to one client and the report settings chosen when the link was created. It does not give the recipient access to the rest of the app.

Common questions​

Do recipients need a Launchpad account?​

No. Anyone with an active link can open the shared report without signing in.

Can a recipient change the report filters?​

No. The link keeps the client, date range, platform filters, sort order, and result limit chosen when it was created.

The standard expiry is 90 days. Links can be created with a different allowed expiry, and an active link can be revoked.

The link is invalid, expired, or revoked. No report data is shown in that case.

Can I share a combined multi-client report?​

No. A shared link is always for exactly one client.

How to use it​

  1. In Best ads, select one client and set the report filters you want to share.
  2. Create the share link and copy the URL when it is shown.
  3. Send the URL to the recipient.
  4. Revoke the link when it should no longer be available.
For developers — routes, guards, and internals
  • Route: /share/[token]
  • Guard: none — deliberately public and unauthenticated. /share is on the segment-scoped PUBLIC_PREFIXES allow-list in src/lib/public-paths.ts; the token in the path IS the authorization.

No screenshot. Not captured: tokens are stored sha256-hashed (src/lib/report-share.ts), so no existing plaintext token was recoverable read-only, and minting a fresh report_share_tokens row is a DB write that the read-only capture run was barred from. To capture later: mint from the Best Ads ShareButton on a single-client result set, screenshot, and redact the token from the URL before publishing.

Public, read-only, tokenized report page — the V2 replacement for legacy Launchpad's metrics-graph-share/{token} and best-ads-report/{token} routes (S5-16 / S5-34 / S5-36). Renders standalone: no AdminSidebarShell, no nav, no auth footer.

Features

  • Two resource types. A token unlocks either a metrics_graph share (the per-day spend/revenue/ROAS/conversions series behind the ad detail drawer's chart, via getSpendSeries) or a best_ads_report share (the Best Ads table, via getBestAds). Rendered by SharedMetricsGraph / SharedBestAdsReport.
  • Token lifecycle. Minted from the Best Ads ShareButton via POST /api/admin/share-links. The plaintext token is 32 random bytes (base64url) returned exactly once at mint; only sha256(token) is persisted in the Supabase report_share_tokens table, so a DB dump cannot be redeemed. Default TTL 90 days, maximum 365; expires_at IS NULL means never-expires (explicit opt-in at the API layer only); revoked_at is the kill switch.
  • Tenant isolation — the token IS the scope. The share row stores a client_id (a clients.slug); resolveShareToken() returns it and shareFilter() feeds it straight into MarketingFilter.clientId. The page deliberately never calls resolveQueryScope() — there is no session, and an authenticated admin opening a share link must see exactly what an anonymous viewer sees, never their own wider scope.
  • Narrowing-only payload. The frozen payload (date window/range, platforms, brands, single adId, sort, limit) is applied as additional AND clauses on top of client_id = {clientId}, so it can only shrink the result set. There is deliberately no clientId field in the payload, and shareFilter() writes clientId last so a malformed payload can never shadow it. No query/search param is read at all.
  • Fail-closed resolution. Unknown token → genuine 404 (notFound()); expired or revoked → an explicit ShareUnavailable "link is no longer active" page. Nothing is queried and no partial report renders in either case.
  • No session grant. Unlike magic links, a share token grants no session and no auth — one frozen read-only report scope and nothing else.
  • View audit. recordShareView(share.id) is fired best-effort and deliberately not awaited, so a token holder cannot amplify DB write load by hammering the route; it swallows its own errors.
  • Leak hygiene. Page metadata sets robots: noindex, nofollow and referrer: no-referrer — the token lives in the URL path, so without the referrer policy any outbound request would forward the plaintext bearer token in the Referer header.

Key files

KindPath
Pagesrc/app/share/[token]/page.tsx
Token libsrc/lib/report-share.ts (resolveShareToken, shareFilter, recordShareView)
Public componentssrc/components/share/{SharedMetricsGraph,SharedBestAdsReport,ShareUnavailable}.tsx
Mint affordancesrc/components/share/ShareButton.tsx (on /best-ads)
Allow-listsrc/lib/public-paths.ts ('/share' entry)
API/api/admin/share-links (mint); page itself queries ClickHouse server-side via queries.ts