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.
How long does a shared link work?
The standard expiry is 90 days. Links can be created with a different allowed expiry, and an active link can be revoked.
What does it mean if the link is unavailable?
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
- In Best ads, select one client and set the report filters you want to share.
- Create the share link and copy the URL when it is shown.
- Send the URL to the recipient.
- 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.
/shareis on the segment-scopedPUBLIC_PREFIXESallow-list insrc/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 freshreport_share_tokensrow 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_graphshare (the per-day spend/revenue/ROAS/conversions series behind the ad detail drawer's chart, viagetSpendSeries) or abest_ads_reportshare (the Best Ads table, viagetBestAds). Rendered bySharedMetricsGraph/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; onlysha256(token)is persisted in the Supabasereport_share_tokenstable, so a DB dump cannot be redeemed. Default TTL 90 days, maximum 365;expires_at IS NULLmeans never-expires (explicit opt-in at the API layer only);revoked_atis the kill switch. - Tenant isolation — the token IS the scope. The share row stores a
client_id(aclients.slug);resolveShareToken()returns it andshareFilter()feeds it straight intoMarketingFilter.clientId. The page deliberately never callsresolveQueryScope()— 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, singleadId, sort, limit) is applied as additional AND clauses on top ofclient_id = {clientId}, so it can only shrink the result set. There is deliberately no clientId field in the payload, andshareFilter()writesclientIdlast 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 explicitShareUnavailable"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, nofollowandreferrer: no-referrer— the token lives in the URL path, so without the referrer policy any outbound request would forward the plaintext bearer token in theRefererheader.
Key files
| Kind | Path |
|---|---|
| Page | src/app/share/[token]/page.tsx |
| Token lib | src/lib/report-share.ts (resolveShareToken, shareFilter, recordShareView) |
| Public components | src/components/share/{SharedMetricsGraph,SharedBestAdsReport,ShareUnavailable}.tsx |
| Mint affordance | src/components/share/ShareButton.tsx (on /best-ads) |
| Allow-list | src/lib/public-paths.ts ('/share' entry) |
| API | /api/admin/share-links (mint); page itself queries ClickHouse server-side via queries.ts |