> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Share Links

> Track form response sources with custom shareable links

# Share Links

> Create named, trackable links for distributing forms to different audiences.

Share links let you create multiple URLs for the same form, each with its own source tracking. This helps you understand where your responses are coming from—schools, partner organizations, email campaigns, etc.

***

## Why Use Share Links?

| Use Case             | Example                                            |
| -------------------- | -------------------------------------------------- |
| Partner distribution | Create a link for each school or community partner |
| Campaign tracking    | Track responses from email vs. social media        |
| Time-limited access  | Links that auto-expire after a deadline            |
| A/B testing          | Different links for different outreach messages    |

***

## Link URL Structure

Share links use a `src` query parameter to identify the source:

```
https://terra.withunify.org/f/rental-assistance?src=Ab3kL9Zq
```

| Component              | Description                           |
| ---------------------- | ------------------------------------- |
| `/f/rental-assistance` | Form's public URL (by slug)           |
| `?src=Ab3kL9Zq`        | Share link's unique slug (8-10 chars) |

When an applicant submits through this URL, the submission is linked to the share link for tracking.

***

## Creating Share Links

From the form settings page, navigate to **Share Links** and click **New Link**.

### Required Fields

| Field    | Description                                                  |
| -------- | ------------------------------------------------------------ |
| **Name** | Human-readable identifier (e.g., "South Kitsap High School") |

### Optional Fields

| Field               | Description                           | Default                  |
| ------------------- | ------------------------------------- | ------------------------ |
| **UTM Source**      | Analytics tracking parameter          | Auto-generated from name |
| **Expiration Date** | Auto-close after this date            | Never expires            |
| **Response Limit**  | Max submissions this link can receive | Unlimited                |

### Server Action

```typescript theme={null}
import { createShareLink } from "@/app/actions/share-links";

const result = await createShareLink({
  form_id: "uuid",
  name: "South Kitsap High School",
  utm_source: "south-kitsap-hs",  // Optional, defaults to slugified name
  expires_at: "2025-03-31T23:59:59Z",  // Optional
  response_limit: 50,  // Optional, cap submissions
});

// result.link contains the created share link
```

***

## UTM Source Tracking

Each share link has a `utm_source` value for analytics integration. This is stored with every submission made through that link.

### Auto-Generation

If not specified, the UTM source is generated from the link name:

```
"South Kitsap High School" → "south-kitsap-hs"
```

### How It's Stored

```typescript theme={null}
interface Submission {
  share_link_id: string | null;  // Links to form_share_links.id
  // ...
}
```

The `share_link_id` foreign key connects submissions to their source link, enabling response counts and attribution reporting.

***

## Link Expiration

Share links can be set to automatically stop accepting submissions after a specific date.

### How It Works

* Set an expiration date when creating the link
* After the date passes, the link shows as "Expired"
* Applicants visiting an expired link see the form-closed message
* No cron job required—expiration is checked at access time

### Expiration Check

```typescript theme={null}
export async function getShareLinkBySlug(slug: string) {
  const { data } = await supabaseAdmin
    .from("form_share_links")
    .select("*")
    .eq("slug", slug)
    .single();

  // Check if manually closed
  if (!data.is_open) {
    return { link: data, isClosed: true };
  }

  // Check if expired
  if (data.expires_at && new Date(data.expires_at) < new Date()) {
    return { link: data, isClosed: true };
  }

  return { link: data, isClosed: false };
}
```

<Note>
  Expiration doesn't modify the database record—the link is evaluated as "closed" dynamically. You can remove the expiration date to reopen an expired link.
</Note>

***

## Open/Close Toggle

Each share link can be manually toggled open or closed, independent of the main form status.

| Link Status | Form Status | Result                 |
| ----------- | ----------- | ---------------------- |
| Open        | Published   | ✅ Accepts submissions  |
| Closed      | Published   | ❌ Shows closed message |
| Open        | Closed      | ❌ Shows closed message |
| Expired     | Published   | ❌ Shows closed message |
| At Limit    | Published   | ❌ Shows closed message |

### Toggle Action

```typescript theme={null}
import { toggleShareLink } from "@/app/actions/share-links";

// Close a link
await toggleShareLink(linkId, false);

// Reopen a link
await toggleShareLink(linkId, true);
```

***

## Response Limits

Cap the number of submissions a link can receive—useful for limited capacity events or first-come-first-served scenarios.

### How It Works

* Set a response limit when creating the link (e.g., 50)
* The UI shows progress: "23/50" in the Responses column
* When the limit is reached, the link shows "At Limit" badge
* Applicants visiting a full link see the form-closed message

### Limit Check

```typescript theme={null}
// Check if response limit has been reached
if (data.response_limit) {
  const { data: count } = await supabaseAdmin.rpc("get_share_link_response_count", {
    link_id: data.id,
  });
  if (count && count >= data.response_limit) {
    return { link: data, isClosed: true, closedReason: "limit_reached" };
  }
}
```

***

## Response Tracking

Each share link tracks the number of submissions it has received.

### Counting Responses

```sql theme={null}
-- RPC function: get_share_link_response_count
SELECT COUNT(*) FROM submissions
WHERE share_link_id = $link_id
  AND status != 'draft'
  AND deleted_at IS NULL;
```

This count appears in the share links table and updates in real-time as submissions come in.

***

## Data Model

### form\_share\_links Table

```typescript theme={null}
interface FormShareLink {
  id: string;
  form_id: string;
  name: string;
  slug: string;                // Unique 8-10 char identifier
  utm_source: string | null;
  utm_medium: string | null;   // Default: "share_link"
  utm_campaign: string | null;
  is_open: boolean;            // Default: true
  expires_at: string | null;   // Optional expiration
  response_limit: number | null; // Optional submission cap
  created_at: string;
  updated_at: string;
  response_count?: number;     // Computed, not stored
}
```

### Database Schema

```sql theme={null}
CREATE TABLE form_share_links (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  form_id UUID NOT NULL REFERENCES forms(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL UNIQUE,
  utm_source TEXT,
  utm_medium TEXT DEFAULT 'share_link',
  utm_campaign TEXT,
  is_open BOOLEAN DEFAULT true,
  expires_at TIMESTAMPTZ,
  response_limit INTEGER,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for efficient slug lookups
CREATE INDEX idx_form_share_links_slug ON form_share_links(slug);

-- Index for form-based queries
CREATE INDEX idx_form_share_links_form_id ON form_share_links(form_id);

-- Index for expiration checks
CREATE INDEX idx_form_share_links_expires_at
  ON form_share_links(expires_at)
  WHERE expires_at IS NOT NULL;
```

***

## Server Actions

All share link operations are available as server actions:

| Action                            | Description                                   |
| --------------------------------- | --------------------------------------------- |
| `listShareLinks(formId)`          | Get all links for a form with response counts |
| `createShareLink(input)`          | Create a new share link                       |
| `updateShareLink(input)`          | Update link properties                        |
| `toggleShareLink(id, isOpen)`     | Quick open/close toggle                       |
| `renameShareLink(id, name)`       | Rename a link (UTM stays locked)              |
| `duplicateShareLink(id, newName)` | Copy link with new name and UTM               |
| `deleteShareLink(id)`             | Remove a share link                           |
| `getShareLinkBySlug(slug)`        | Get link by slug (public route)               |

### Authorization

All actions (except `getShareLinkBySlug`) require form admin access via `checkFormAccess()`.

```typescript theme={null}
const access = await checkFormAccess(formId);
if (!access.success) {
  return { success: false, error: access.error };
}
```

***

## UI Components

The share links management UI is located at:

```
/forms/[formId]/settings/share-links
```

### Features

| Feature          | Description                                                      |
| ---------------- | ---------------------------------------------------------------- |
| Link table       | Shows name, UTM source, created date, expiration, response count |
| URL tooltip      | Hover over link name to see full URL                             |
| Status badges    | Visual indicators for Closed, Expired, and At Limit links        |
| Quick actions    | Rename, duplicate, toggle, copy URL, delete                      |
| Create dialog    | Form for new links with UTM, expiration, and response limit      |
| Rename dialog    | Update link name while keeping UTM locked                        |
| Duplicate dialog | Copy link with new name and auto-generated UTM                   |

### Copying Links

The copy button generates the full URL:

```typescript theme={null}
const url = `${window.location.origin}/f/${formSlug}?src=${link.slug}`;
await navigator.clipboard.writeText(url);
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use descriptive names">
    Name links after the distribution channel or partner: "Lincoln High School", "January Email Blast", "Community Center Flyers"
  </Accordion>

  <Accordion title="Set expiration for time-sensitive campaigns">
    If you're doing targeted outreach with a deadline, set the expiration to match. This prevents late submissions without manual intervention.
  </Accordion>

  <Accordion title="Use response limits for capacity">
    When distributing to multiple partners with limited slots each, set response limits. For example, give each school a link with a 50-response limit for a first-come-first-served program.
  </Accordion>

  <Accordion title="Duplicate for similar partners">
    Use the duplicate feature when creating links for similar partners. It copies expiration and response limit settings while generating a fresh UTM source.
  </Accordion>

  <Accordion title="Keep UTM sources consistent">
    Use a consistent naming convention for UTM sources across your forms. This makes cross-form analytics easier.
  </Accordion>

  <Accordion title="Don't delete links with responses">
    While you can delete share links, existing submissions keep their `share_link_id`. Deleting removes the link name from reports, making attribution harder.
  </Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="Publishing" icon="rocket" href="/features/form-lifecycle/publishing">
    Form publishing workflow
  </Card>

  <Card title="Submissions" icon="inbox" href="/features/form-lifecycle/submissions">
    How submissions work
  </Card>

  <Card title="Public Pages" icon="globe" href="/features/form-lifecycle/public-pages">
    Applicant-facing forms
  </Card>

  <Card title="Branding" icon="palette" href="/features/branding">
    Form styling options
  </Card>
</CardGroup>
