Skip to content

[Feature]: Add link expiration support — allow users to set an optional TTL (time-to-live) when creating a short link so expired links automatically return 410 Gone #305

Description

@divyanshim27

Summary

piik.me currently creates permanent short links with no expiration mechanism. For marketing campaigns, time-limited promotions, or security-conscious users who want short links to stop working after a certain date, there is no way to set an expiry on a link. This is a standard feature in every major URL shortener (Bitly, TinyURL, Rebrandly) and is a meaningful differentiator for a "professional-grade" platform.

Problem

  • All links created via POST /api/shorten are permanent — there is no expiresAt field in the links Firestore collection schema.
  • Users who share links for time-sensitive content (event registrations, limited-time offers, one-time download links) have no way to deactivate a link automatically.
  • From a security perspective, short links to internal resources shared temporarily (e.g., a 24-hour Zoom link) remain permanently accessible even after the underlying resource is gone.
  • The GET /:shortCode redirect handler has no TTL check — even if a user manually wanted to set an expiry, there is no enforcement.
  • Competitor analysis: Bitly's free tier supports link expiration; piik.me has no equivalent.

Proposed Solution

1. Update the Firestore links schema to include expiresAt:

The links collection document gains an optional field:

{
  originalUrl: string,
  shortCode: string,
  shortUrl: string,
  userId: string,
  userEmail: string,
  createdAt: timestamp,
  expiresAt: timestamp | null,  // NEW — null means never expires
  utmParams: { ... }
}

2. Update POST /api/shorten to accept expiresAt:

// src/routes/links.routes.js (or equivalent)
router.post('/shorten', authMiddleware, async (req, res) => {
  const { originalUrl, customCode, expiresIn, utmParams } = req.body;
  // expiresIn: optional, in hours (e.g., 24 = expires in 24 hours)

  let expiresAt = null;
  if (expiresIn && Number.isInteger(expiresIn) && expiresIn > 0 && expiresIn <= 8760) {
    // Cap at 1 year (8760 hours)
    expiresAt = new Date(Date.now() + expiresIn * 60 * 60 * 1000);
  }

  // ... existing link creation logic ...
  await db.collection('links').doc(shortCode).set({
    // ... existing fields ...
    expiresAt: expiresAt ? admin.firestore.Timestamp.fromDate(expiresAt) : null,
  });
});

3. Add expiry check in the redirect handler:

// server.js or routes/redirect.routes.js
app.get('/:shortCode', async (req, res) => {
  const { shortCode } = req.params;

  // Skip bio link slugs and API routes
  if (shortCode.startsWith('api') || shortCode.startsWith('u')) {
    return next();
  }

  const linkDoc = await db.collection('links').doc(shortCode).get();

  if (!linkDoc.exists) {
    return res.status(404).send('Link not found.');
  }

  const link = linkDoc.data();

  // Check expiry
  if (link.expiresAt && link.expiresAt.toDate() < new Date()) {
    return res.status(410).send('This link has expired.');
  }

  // ... existing redirect + analytics tracking logic ...
  res.redirect(link.originalUrl);
});

4. Frontend UI changes in public/js/app.js:

  • Add an optional "Link Expiration" dropdown in the create-link form: [ Never ] [ 24 hours ] [ 7 days ] [ 30 days ] [ Custom date ]
  • Show the expiry date/time in the link dashboard if set.
  • Display an "EXPIRED" badge on expired links in the user's link list.

Additional Notes

  • The 410 Gone HTTP status code is the semantically correct response for expired/deleted resources — preferable to 404 because it signals the resource existed but is intentionally no longer available.
  • expiresAt: null is the default — all existing links are unaffected by this change.
  • A Firestore scheduled Cloud Function (or a Vercel cron job) could batch-delete expired links nightly to keep the database clean — this is a separate follow-up.

Could you assign this issue to me?

Labels: enhancement, feature, backend, GSSoC 2026

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions