Skip to main content

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.
Use CaseExample
Partner distributionCreate a link for each school or community partner
Campaign trackingTrack responses from email vs. social media
Time-limited accessLinks that auto-expire after a deadline
A/B testingDifferent links for different outreach messages

Share links use a src query parameter to identify the source:
https://terra.withunify.org/f/rental-assistance?src=Ab3kL9Zq
ComponentDescription
/f/rental-assistanceForm’s public URL (by slug)
?src=Ab3kL9ZqShare link’s unique slug (8-10 chars)
When an applicant submits through this URL, the submission is linked to the share link for tracking.
From the form settings page, navigate to Share Links and click New Link.

Required Fields

FieldDescription
NameHuman-readable identifier (e.g., “South Kitsap High School”)

Optional Fields

FieldDescriptionDefault
UTM SourceAnalytics tracking parameterAuto-generated from name
Expiration DateAuto-close after this dateNever expires
Response LimitMax submissions this link can receiveUnlimited

Server Action

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

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.
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

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 };
}
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.

Open/Close Toggle

Each share link can be manually toggled open or closed, independent of the main form status.
Link StatusForm StatusResult
OpenPublished✅ Accepts submissions
ClosedPublished❌ Shows closed message
OpenClosed❌ Shows closed message
ExpiredPublished❌ Shows closed message
At LimitPublished❌ Shows closed message

Toggle Action

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

// 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

-- 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

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

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:
ActionDescription
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().
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

FeatureDescription
Link tableShows name, UTM source, created date, expiration, response count
URL tooltipHover over link name to see full URL
Status badgesVisual indicators for Closed, Expired, and At Limit links
Quick actionsRename, duplicate, toggle, copy URL, delete
Create dialogForm for new links with UTM, expiration, and response limit
Rename dialogUpdate link name while keeping UTM locked
Duplicate dialogCopy link with new name and auto-generated UTM
The copy button generates the full URL:
const url = `${window.location.origin}/f/${formSlug}?src=${link.slug}`;
await navigator.clipboard.writeText(url);

Best Practices

Name links after the distribution channel or partner: “Lincoln High School”, “January Email Blast”, “Community Center Flyers”
If you’re doing targeted outreach with a deadline, set the expiration to match. This prevents late submissions without manual intervention.
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.
Use the duplicate feature when creating links for similar partners. It copies expiration and response limit settings while generating a fresh UTM source.
Use a consistent naming convention for UTM sources across your forms. This makes cross-form analytics easier.