share-link
Token-based public sharing — create, access, and revoke shareable URLs for private content with optional expiry. Use this skill when the user says "add share link", "public sharing", "shareable url", "share with client", or "public link".
What this skill does
# Share Link
Token-based public sharing system that lets authenticated users generate shareable URLs for private content. Tokens are URL-safe, 12 characters, and support optional expiry and view-count limits. Revocation sets a `revokedAt` timestamp so history is preserved.
## Prerequisites
- Next.js app with App Router (no `src/` directory)
- `@/lib/auth` exporting `auth` (better-auth)
- `@/db` exporting `db` (Drizzle + Postgres)
- `db/schema/index.ts` exporting all schema tables
- `NEXT_PUBLIC_APP_URL` set in environment
## Installation
No new packages required. Uses Node.js built-in `crypto`.
## What Gets Created
```
app/
└── api/
└── share/
├── route.ts POST create link, GET list my links
└── [token]/
└── route.ts GET public access, DELETE revoke
db/
└── schema/
└── shared-links.ts
lib/
└── share-link/
└── index.ts
```
## Setup Steps
### Step 1: Create `db/schema/shared-links.ts`
```typescript
import { integer, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
export const sharedLinks = pgTable("shared_links", {
id: uuid("id").primaryKey().defaultRandom(),
userId: text("user_id").notNull(),
token: text("token").notNull().unique(),
resourceType: text("resource_type").notNull(),
resourceId: text("resource_id").notNull(),
resourceData: jsonb("resource_data").notNull(),
label: text("label"),
expiresAt: timestamp("expires_at"),
viewCount: integer("view_count").default(0).notNull(),
maxViews: integer("max_views"),
createdAt: timestamp("created_at").defaultNow().notNull(),
revokedAt: timestamp("revoked_at"),
})
```
### Step 2: Export from `db/schema/index.ts`
Add this export to your existing `db/schema/index.ts`:
```typescript
export * from "./shared-links"
```
### Step 3: Create `lib/share-link/index.ts`
```typescript
import type { sharedLinks } from "@/db/schema"
import type { InferSelectModel } from "drizzle-orm"
export type ShareLink = InferSelectModel<typeof sharedLinks>
export function generateShareUrl(token: string): string {
const base = process.env.NEXT_PUBLIC_APP_URL ?? ""
return `${base}/share/${token}`
}
export function isExpired(link: ShareLink): boolean {
if (!link.expiresAt) return false
return link.expiresAt < new Date()
}
export function isRevoked(link: ShareLink): boolean {
return link.revokedAt !== null
}
export function isOverLimit(link: ShareLink): boolean {
if (link.maxViews === null || link.maxViews === undefined) return false
return link.viewCount >= link.maxViews
}
export function isAccessible(link: ShareLink): boolean {
return !isRevoked(link) && !isExpired(link) && !isOverLimit(link)
}
```
### Step 4: Create `app/api/share/route.ts`
```typescript
import { db } from "@/db"
import { sharedLinks } from "@/db/schema"
import { auth } from "@/lib/auth"
import { generateShareUrl } from "@/lib/share-link"
import { eq, and, isNull, or, gt } from "drizzle-orm"
import { headers } from "next/headers"
import { NextResponse } from "next/server"
import { randomBytes } from "node:crypto"
function generateToken(): string {
return randomBytes(9).toString("base64url")
}
export async function POST(request: Request): Promise<NextResponse> {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 })
}
const parsed = body as Record<string, unknown>
if (
typeof parsed.resourceType !== "string" ||
typeof parsed.resourceId !== "string" ||
parsed.resourceData === undefined
) {
return NextResponse.json(
{ error: "resourceType, resourceId, and resourceData are required" },
{ status: 400 },
)
}
const token = generateToken()
const expiresAt =
typeof parsed.expiresAt === "string" ? new Date(parsed.expiresAt) : null
const maxViews =
typeof parsed.maxViews === "number" ? parsed.maxViews : null
const label =
typeof parsed.label === "string" ? parsed.label : null
const [row] = await db
.insert(sharedLinks)
.values({
userId: session.user.id,
token,
resourceType: parsed.resourceType,
resourceId: parsed.resourceId,
resourceData: parsed.resourceData as Record<string, unknown>,
label,
expiresAt,
maxViews,
})
.returning({
id: sharedLinks.id,
token: sharedLinks.token,
expiresAt: sharedLinks.expiresAt,
})
return NextResponse.json({
id: row.id,
token: row.token,
url: generateShareUrl(row.token),
expiresAt: row.expiresAt,
})
}
export async function GET(): Promise<NextResponse> {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const now = new Date()
const links = await db
.select()
.from(sharedLinks)
.where(
and(
eq(sharedLinks.userId, session.user.id),
isNull(sharedLinks.revokedAt),
or(isNull(sharedLinks.expiresAt), gt(sharedLinks.expiresAt, now)),
),
)
const result = links.map((link) => ({
...link,
url: generateShareUrl(link.token),
}))
return NextResponse.json(result)
}
```
### Step 5: Create `app/api/share/[token]/route.ts`
```typescript
import { db } from "@/db"
import { sharedLinks } from "@/db/schema"
import { auth } from "@/lib/auth"
import { isAccessible, isExpired, isOverLimit, isRevoked } from "@/lib/share-link"
import { eq } from "drizzle-orm"
import { headers } from "next/headers"
import { NextResponse } from "next/server"
type RouteContext = {
params: Promise<{ token: string }>
}
export async function GET(
_request: Request,
{ params }: RouteContext,
): Promise<NextResponse> {
const { token } = await params
const [link] = await db
.select()
.from(sharedLinks)
.where(eq(sharedLinks.token, token))
.limit(1)
if (!link) {
return NextResponse.json({ error: "Not found" }, { status: 404 })
}
if (isRevoked(link) || isExpired(link)) {
return NextResponse.json(
{ error: "This link has expired or been revoked" },
{ status: 410 },
)
}
if (isOverLimit(link)) {
return NextResponse.json(
{ error: "This link has reached its maximum view count" },
{ status: 429 },
)
}
await db
.update(sharedLinks)
.set({ viewCount: (link.viewCount ?? 0) + 1 })
.where(eq(sharedLinks.token, token))
return NextResponse.json({
resourceType: link.resourceType,
resourceId: link.resourceId,
resourceData: link.resourceData,
label: link.label,
viewCount: (link.viewCount ?? 0) + 1,
createdAt: link.createdAt,
})
}
export async function DELETE(
_request: Request,
{ params }: RouteContext,
): Promise<NextResponse> {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { token } = await params
const [link] = await db
.select()
.from(sharedLinks)
.where(eq(sharedLinks.token, token))
.limit(1)
if (!link) {
return NextResponse.json({ error: "Not found" }, { status: 404 })
}
if (link.userId !== session.user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
}
await db
.update(sharedLinks)
.set({ revokedAt: new Date() })
.where(eq(sharedLinks.token, token))
return NextResponse.json({ success: true })
}
```
### Step 6: Push the schema
```bash
bunx drizzle-kit push
```
## Usage
```typescript
// Create a share link (authenticated)
const res = await fetch("/api/share", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
resourceType: "storyboard",
resoRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.