Public Share Links & Permissioned Sharing — Chat Prompts
If your product has any kind of artifact users create — a doc, a board, a dashboard, a video, a form, a report, a workspace — sooner or later users want to share it. The naive shape: "make it public" toggle. The right shape: a real permission model with link-level access control, viewer/commenter/editor distinctions, expiry, password protection, audit, and revocation. Done well, share links become a viral growth loop (every share is a landing page for a new user). Done badly, share links become your worst security incident (the doc was "private to the team" but actually visible to anyone with the URL because of an old experiment).
This chat walks through implementing public share links + permissioned sharing in a SaaS app: data model, link generation, access control middleware, UI surfaces (share modal, public viewer, sign-in upgrade flow), expiry/rotation, audit, security, and growth instrumentation.
What you're building
- Per-resource share link with unguessable token
- Link-level permissions (viewer / commenter / editor)
- Optional password protection
- Optional expiry
- Optional sign-in requirement (signed-in-only or anyone-with-link)
- Public-page rendering with sign-in upgrade prompt
- Audit log of share creation, access, revocation
- Revocation flow (one-click)
- Workspace-level sharing policies (admin can disable public sharing org-wide)
- Growth instrumentation (track shares as virality signal)
1. Design the share-link data model
Help me design the data model for public share links in a [Postgres / Supabase / DrizzleORM / Prisma] schema.
Product context:
- I'm building [my product]
- Shareable resource type: [doc / board / dashboard / form / report]
- I need to support: anyone-with-link viewing, optional password, optional expiry, optional commenter/editor access, signed-in-only, public
I want a shape like:
share_link:
id (uuid)
resource_type (enum: doc, board, dashboard, etc.)
resource_id (uuid; references the resource)
token (string; unguessable; 32+ chars, base64url)
permission (enum: view, comment, edit)
scope (enum: public, signed_in_required, password_protected, workspace_only)
password_hash (nullable; argon2 hash of password if scope=password_protected)
expires_at (nullable timestamp)
created_by (user_id)
created_at
revoked_at (nullable)
revoked_by (nullable; user_id)
last_accessed_at (nullable)
access_count (int; denormalized counter)
share_link_access_log:
id
share_link_id
accessed_at
accessor_user_id (nullable; if signed in)
accessor_ip_hash (sha256 of IP; for privacy)
accessor_user_agent (truncated)
granted (boolean; did access succeed)
reason (string; e.g. "expired", "wrong-password", "ok")
Constraints:
- Tokens MUST be unguessable (>=128 bits of entropy)
- Tokens MUST be unique
- One resource can have multiple share links (different permissions, different expiry, different recipients)
- Soft-revocation (set revoked_at) preferred over hard delete (audit trail)
For each table:
1. Show me the migration SQL
2. Show me indexes (token lookup is hot path)
3. Show me the [Drizzle / Prisma] schema
4. Show me TypeScript types
5. Show me row-level security policies if [Supabase / Postgres RLS]
Then explain:
- Why argon2 over bcrypt for password hashing here (or vice versa)
- Why I should hash the IP rather than store it raw
- How to handle token rotation (replace token vs revoke + new link)
- Why scope=workspace_only is useful (link works only for org members signed in)
Output: schema migration + ORM types + RLS policies + index strategy.
2. Implement the token generation + lookup primitives
Now write the core token generation and lookup primitives for share links.
Stack: [Next.js + TypeScript / Node / your stack]
Functions I need:
1. generateShareToken(): string
- Uses crypto.randomBytes (Node) or crypto.getRandomValues (Web)
- 32 bytes (256 bits)
- Encoded as base64url (URL-safe, no padding)
- Returns string like "Xy3K9pQ-aB...-zN" (~43 chars)
2. createShareLink(input: {
resourceType: ResourceType
resourceId: string
permission: 'view' | 'comment' | 'edit'
scope: 'public' | 'signed_in_required' | 'password_protected' | 'workspace_only'
password?: string // plaintext; we hash before storing
expiresAt?: Date
createdByUserId: string
}): Promise<ShareLink>
- Validates scope/password combination
- Hashes password with argon2id if provided
- Inserts row
- Returns the share link record with shareUrl computed (e.g. https://app.com/share/<token>)
3. resolveShareLink(token: string): Promise<ShareLinkResolution>
- Looks up by token (indexed)
- Returns one of:
- { status: "not_found" }
- { status: "revoked" }
- { status: "expired" }
- { status: "password_required", linkId }
- { status: "signin_required", linkId }
- { status: "workspace_only", workspaceId }
- { status: "ok", link, resource }
- Should be O(1) on the token lookup
- Should NOT log access yet (caller logs on success)
4. validateSharePassword(linkId: string, password: string): Promise<boolean>
- Constant-time comparison (use argon2.verify, not ===)
5. revokeShareLink(linkId: string, revokedByUserId: string): Promise<void>
- Sets revoked_at, revoked_by
- Does NOT delete (audit)
6. logShareAccess(linkId: string, ctx: AccessContext): Promise<void>
- Hash IP with sha256 + secret salt (env var)
- Truncate user agent to 200 chars
- Insert into share_link_access_log
- Increment access_count + update last_accessed_at on share_links (single UPDATE)
Security constraints:
- Token comparison MUST use constant-time (timingSafeEqual)
- Don't log full IP; hash it
- Don't expose share token in URLs that might leak (e.g. don't put in Referer-leaking redirects)
- Rate-limit token resolution per IP (preventing token enumeration attacks)
Show me the implementation with proper types.
Output: working primitives ready to wire into routes.
3. Build the public share-page route + middleware
Now wire up the public share-page route at /share/[token].
Stack: Next.js App Router + Server Components.
Behavior I want:
1. /share/[token] is a public route (no auth required to LOAD)
2. Server Component fetches the share link via resolveShareLink
3. Branches based on resolution status:
- not_found → render <NotFoundPage /> (no link to anything; don't reveal whether token was valid)
- revoked → render <RevokedPage /> (generic; "this link is no longer active")
- expired → render <ExpiredPage />
- password_required → render <PasswordPromptForm linkId={...} /> (POSTs to /share/[token]/unlock)
- signin_required → render <SignInPrompt redirectAfter={shareUrl} />
- workspace_only → if user signed in + in workspace, render resource; else SignInPrompt
- ok → render the resource in read-only / commenter / editor mode based on permission
4. After rendering, fire-and-forget log access (await Promise.all)
5. Resource viewer behaviors:
- permission=view: read-only render, no edit affordances visible
- permission=comment: read-only + comment composer; comments tagged "via share link"
- permission=edit: full editor; show "shared link editor" badge
6. If user is signed in AND signed-in user has direct access (not via the link), prefer their direct permissions (don't downgrade them)
Code organization:
- /share/[token]/page.tsx — server component, dispatches based on status
- /share/[token]/unlock/route.ts — POST handler for password
- /share/[token]/_components/ — private components (PasswordPrompt, SignInPrompt, etc.)
- middleware.ts — adds Cache-Control: private, no-store for /share/* (prevent CDN caching of share pages)
Important security:
- /share/[token] response MUST set Cache-Control: private, no-store (CDN must not cache; tokens are sensitive)
- /share/[token] MUST NOT include the token in HTML body that gets logged anywhere
- Robots: include <meta name="robots" content="noindex"> by default unless link is public AND user opted in to public-indexing
Walk me through:
1. The page.tsx implementation
2. The unlock route handler (with rate limiting)
3. The middleware Cache-Control rules
4. How to render the resource viewer in degraded permission mode
Output: a working public share page that gracefully handles all states.
4. Build the share modal UI
Now build the share modal that users see when they click "Share" on a resource.
Design specs:
- Triggered by a "Share" button in the resource's top-right
- Modal, not a popover (heavier UI; lots of options)
- Tabs or sections:
1. "Share with people" — invite specific users by email; gives them workspace access
2. "Anyone with the link" — generates a public share link
3. "Workspace access" — role-level access (read-only summary; managed elsewhere)
Focus on the "Anyone with the link" section:
UI elements:
- Toggle: "Share via link" (off by default; on when there's an active link)
- Token URL display (copy-to-clipboard button)
- Permission dropdown: "Can view" / "Can comment" / "Can edit" (defaults to view)
- Scope dropdown: "Public" / "Signed-in users only" / "Password-protected" / "Workspace only"
- Password input (visible only when scope=password_protected)
- Expiry dropdown: "No expiry" / "1 day" / "7 days" / "30 days" / "Custom date"
- "Revoke link" button (destructive; confirms)
- "Generate new link" button (rotates token; old link breaks)
- Footer: small text + link to "Manage all share links" (admin view)
State management:
- React Server Components + Server Actions (Next.js)
- OR: TanStack Query + REST endpoints
- Optimistic updates for permission/scope changes
Permission changes:
- Changing permission/scope/expiry should NOT rotate the token (link stays the same)
- Only "Generate new link" or "Revoke" rotates the token
- This matters because users have copied the link into emails / docs
Mobile:
- Full-screen sheet on mobile
- Same controls; vertical layout
Accessibility:
- Modal trapped focus
- Esc closes
- Clear error states
- Copy-to-clipboard with success feedback (toast or inline check icon)
- Permission dropdown labeled, keyboard navigable
Implement:
1. The ShareModal component with all controls
2. The Server Action / API endpoint for create/update/revoke
3. The optimistic UI updates
4. Toast notifications on success/failure
5. Permission/scope changes are a single-click toggle (no need to "Save" — auto-applies)
Output: a polished share modal users will recognize from Notion/Figma/Linear.
5. Implement workspace-level sharing policies
Now add workspace-level admin controls for sharing policies.
Many enterprise customers will require:
- Disable public sharing entirely (force scope=signed_in_required or workspace_only)
- Require password on all public links
- Force expiry within N days
- Disable "anyone with the link" (force email-invite only)
- Domain restrictions ("only people from acme.com can access shared links")
Schema additions:
workspace_sharing_policy:
workspace_id (pk)
allow_public_links (bool; default true)
require_password_on_public (bool; default false)
max_expiry_days (int; null = no max)
allow_external_domain (bool; default true)
allowed_email_domains (string[]; e.g. ['acme.com', 'subsidiary.com'])
updated_at
updated_by
Behavior:
- Share modal UI READS this policy and disables/restricts options accordingly
- Share-link CREATION on the server validates against the policy
- Existing links may grandfather-in OR be marked "non-compliant" (admin choice)
- Admin UI at /settings/sharing exposes these toggles
- Audit log entry on policy change
UX:
- When user attempts to create a link that violates policy: clear error message
"Public sharing is disabled by your workspace admin. Use 'Signed-in users only' instead."
- When admin changes policy: option to retroactively revoke non-compliant links
"23 existing public links will become non-compliant. Revoke them now?"
Implement:
1. The schema migration
2. The policy-check function (validateShareLinkAgainstPolicy)
3. The admin settings page UI (toggle controls)
4. The retroactive-revocation flow
5. Server-side enforcement on share-link creation/update
Edge cases:
- What if a user creates a share link, then admin tightens policy?
Default: link grandfathered until manually revoked, but admin sees "non-compliant" badge
- What if max_expiry_days changes from null to 30?
Existing links keep their expiry; new links capped at 30 days
- What if allowed_email_domains is set?
When a signed-in user tries to access a workspace_only link, validate their email domain
Output: enterprise-grade sharing policies that admins control.
6. Build the audit + abuse-prevention layer
Now harden the share-link system against abuse and add audit trails.
Threat model:
1. Token enumeration — attacker tries random tokens hoping to hit one
2. Token leakage — link gets posted to public chat / email / Slack
3. Brute-force password — attacker repeatedly tries passwords on a password-protected link
4. Link sharing post-revocation — "I revoked this 5 minutes ago; how is it still working?"
5. Unauthorized export — viewer scrapes/copies content via shared link
Mitigations to implement:
Token enumeration:
- Rate-limit /share/[token] requests by IP (e.g. 30 / minute)
- Use 256-bit tokens (already in plan)
- Don't differentiate "not_found" vs "revoked" in error responses (same generic page)
Token leakage:
- Optional: integrate with Have-I-Been-Pwned-style scanning (low priority)
- Audit log shows access patterns (sudden spike in accesses → alert admin?)
- "Share link analytics" UI: show recent access list (IP-hashed, time, referrer)
Brute-force password:
- Rate-limit password attempts per linkId (e.g. 5 attempts per 15 minutes)
- Lockout after N failures (15 min cooldown)
- Email link creator on lockout: "Someone is trying to brute-force your share link"
Cache invalidation on revoke:
- /share/[token] uses Cache-Control: no-store (already)
- BUT: in-memory caches (e.g. Redis lookup-cache for token→link) MUST be invalidated on revoke
- Pattern: short TTL (60s) + invalidation pub/sub
Export prevention (limited; not bulletproof):
- For permission=view: disable copy via JS (futile but signals intent)
- For permission=view: add CSS to discourage selection (user-select: none on certain content)
- Watermark with viewer info if signed in (visible overlay)
- Real defense: don't share what you can't afford to leak; the link IS the access
Audit dashboard:
- /settings/sharing/audit page showing recent events:
- Link created (who, what, when, scope)
- Link accessed (count, last access time, IP-hashed list)
- Link revoked (who, when)
- Failed password attempt (count)
- Policy changes
- Filterable by user, resource, date range
- Exportable as CSV (compliance asks)
Implement:
1. The rate-limiting layer (use [Upstash / your rate-limit lib])
2. The Redis cache + invalidation
3. The audit dashboard page
4. The lockout email
5. Watermark component for permission=view rendering
Don't:
- Pretend export-prevention is real security (it's friction, not protection)
- Block legitimate users behind aggressive rate limits (tune carefully)
Output: a hardened sharing system with real audit + abuse prevention.
7. Add growth instrumentation (sharing as a viral loop)
Sharing is the highest-leverage growth surface in many SaaS products. Every share link is a landing page for a new user. Instrument it.
Events to track in [PostHog / Amplitude / Mixpanel]:
share_link_created:
- user_id, workspace_id, resource_type, resource_id
- permission, scope, has_password, has_expiry
- source (modal_button / keyboard_shortcut / api / mobile)
share_link_copied:
- link_id, copied_at
- tracks "intent to share" beyond just creating link
share_link_accessed:
- link_id, accessed_at
- referrer (Slack / Gmail / Twitter / direct / unknown)
- is_signed_in (boolean)
- is_existing_user (signed-in user already in our system)
- is_first_visit (first time this hashed-IP accessed any link)
share_link_signup_prompt_shown:
- link_id, accessor_session_id
share_link_signup_completed:
- link_id, new_user_id, time_to_signup_minutes
share_link_signup_to_workspace_join:
- new_user_id, joined_workspace_id, days_after_signup
KPIs:
- shares per active user per week (engagement leading indicator)
- new signups per share (virality coefficient: K)
- workspace-join-rate from share signups (deeper conversion)
- share-link → signed-up-user conversion rate (target: 8-15% for B2B)
- shares per resource type (which surfaces drive virality)
UX optimizations once you have data:
- Top of public share page: subtle "Sign up free with [Product]" banner
- After 30s viewing: soft modal "Want to make your own?" (don't be aggressive)
- If accessor is on mobile: deep-link to your mobile app (if you have one)
- Personalize: "[Sharer's Name] is using Product to..." (with sharer's permission)
Anti-patterns:
- Forcing signup to view (kills virality; defeats the whole point)
- Auto-attributing the new user to the sharer (don't add referral credit unless intentional referral program)
- Email-harvesting from signup-prompt views (creepy)
Implement:
1. The event tracking instrumentation
2. The KPI dashboard query (PostHog SQL or your equivalent)
3. The signup-banner component on public share pages
4. The "shared by [sharer]" attribution UI (with privacy considerations)
5. A weekly virality report (auto-emailed to product team)
Output: sharing as an instrumented, optimized growth channel.
8. Handle the edge cases
Walk me through edge cases I'll hit and how to handle each:
1. Resource is deleted while a share link still exists
- Show "this content is no longer available" page
- Don't 404 (different signal)
- Allow link creator to see their link is "orphaned" in admin
2. Permission downgrade mid-session (user is editing; admin revokes link)
- Server-side: next save fails with 403
- Client-side: graceful "your access was revoked" modal
- Don't lose their unsaved work — show "copy your changes" affordance
3. User signs in via a share link
- Auto-join workspace? Only if explicitly enabled by admin
- Default: signup → personal workspace; share-link access continues separately
4. Password-protected link emailed alongside the password
- Annoying but common; can't prevent
- At minimum: log this behavior is risky; don't pretend password=protection
5. Same user accesses same link 100x (refresh)
- access_count grows fast; not necessarily abuse
- Dedupe in analytics by sessionId (rate limit at infrastructure)
6. Workspace member accesses a share link to a workspace resource
- Should use their direct permissions, not the link permissions (if higher)
- Don't downgrade workspace_admin to viewer because they clicked a "view" link
7. Public link shared on Twitter; goes viral
- 10K accesses in an hour; unprepared infrastructure
- Caching strategy: Cache-Control: no-store ON link resolution; but resource RENDERING can be cached at the edge IF identical for all viewers
- Have a "go nuclear" admin button: revoke all public share links
8. Bot indexes a share link from a public Slack
- <meta name="robots" content="noindex"> by default
- X-Robots-Tag: noindex header for non-HTML resources
- Cloudflare bot management for high-traffic links
9. Customer wants to bulk-export all share links
- Compliance / GDPR: yes, must support
- /api/share-links/export endpoint (workspace admin only)
- CSV: link_id, resource, permission, scope, created_at, last_accessed, access_count, revoked_at
10. Token in browser history / Referer leakage
- Token is in URL; referrer from /share/[token] → external link will leak it
- Mitigation: <meta name="referrer" content="no-referrer">
- Mitigation: rewrite share URL after consumption (redirect to canonical resource URL if user signs in)
For each, show me the code change + the user-facing copy.
Output: hardened share-link feature that handles real-world conditions.
9. Write the user-facing help docs
Now write the customer-facing help docs for sharing.
I want three docs:
1. "Share a [resource] with someone"
- When to use email invites vs share links
- Step-by-step: open share modal, choose link type, copy
- Permission levels explained (view / comment / edit)
- How to revoke
2. "Sharing security best practices"
- Use password protection for sensitive content
- Set expiry on temporary share
- Don't share password in same email as link
- Revoke when no longer needed
- Workspace policies your admin may enforce
3. "Admin: control sharing in your workspace"
- Where the policy controls are
- When to disable public sharing
- Audit dashboard walkthrough
- GDPR/compliance considerations
Tone: clear, friendly, no jargon. 600-1000 words each.
For each, include:
- Step-by-step screenshots-needed callouts (placeholder; designer adds later)
- "Common questions" section (3-5 FAQs)
- Cross-links to related docs
Write all three docs.
Output: three help-center articles ready to publish.
10. Recap
What you've built:
- Share-link data model with token, permissions, scope, expiry
- Cryptographically strong token generation + constant-time lookup
- Public share page with all access states (not_found / revoked / expired / password / signin / ok)
- Share modal UI with permission, scope, expiry, password
- Workspace admin policies (disable public, require password, max expiry, domain restrict)
- Audit log + access dashboard
- Rate limiting + brute-force protection + lockout emails
- Robots noindex + Cache-Control no-store + referrer policy
- Growth instrumentation (virality K-factor)
- Help docs
What you're explicitly NOT doing in v1:
- DRM / true export prevention (impossible at the browser layer)
- "Email when accessed" notifications (could add v2)
- Per-link analytics dashboard (could add v2)
- Branded share pages (white-label) (enterprise feature, v3+)
- Federation across workspaces (v3+)
Ship v1, learn from real usage, then prioritize v2 from data.
See Also
- Roles & Permissions — workspace permission system this builds on
- API Keys — adjacent token-management problem
- Audit Logs — pairs with share-link access logs
- Rate Limiting & Abuse — brute-force protection patterns
- Cookie Consent — privacy adjacent
- Account Deletion & Data Export — GDPR-adjacent
- Embed Widgets / oEmbed — different way to share content
- Public API — programmatic alternative to share links
- SSO / Enterprise Auth — pairs for enterprise sharing
- Multi-Tenancy — workspace isolation foundation
- Real-Time Collaboration — pairs with edit-permission share
- Comments, Threading, Mentions — pairs with comment-permission share