feat: short-link lifecycle — duplicates, expiry, max-views (builds on #8) #9
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
The user-configurable short links (#3 / #8) need lifecycle controls, per m:
Good news: Shlink supports the last two natively —
validUntil(expiry date) andmaxVisits(visit cap) are short-URL params on the create API. So expiry + max-views are mostly: expose options in the compose flow → pass to Shlink. Low implementation cost. Duplicate-rejection is also already enforced by Shlink (unique slug per domain).Scope
validUntilto Shlink. After it, the slug 404s (card's/c/{token}still works directly — expiry is on the pretty link, not the card, unless we decide otherwise).maxVisitsto Shlink. After N hits the slug stops resolving.sendmy.cards/to/oma-erikacan only exist once, ever. Options:oma-erika-2) and show the sender the final URL.Recommend (a) for MVP (matches #3, no new concepts), with (b) as an easy upgrade. (c) only if m wants true per-user namespaces. Flag for m's decision before building this part.
Constraints
/to//from/slugs and #3's single-segment slugs — keep all working./c/{token}) is the durable artifact; lifecycle limits apply to the pretty short link. (Decide whether an expired/exhausted short link should also imply the card is gone — default: no, only the pretty link expires.)Sequencing
Depends on #8 (same compose + Shlink code path — no parallel worker). otto-head assigns this once #8 is merged. Filed now to capture m's intent.
DoD
validUntil/maxVisits); verified e2e (a maxVisits=1 link 404s on the 2nd hit; an expired link 404s).m's decision on the duplicate policy (PWA, 2026-06-16): Option (a) — first-come-first-served. If a slug is already taken, show a friendly error asking the sender to pick another (same behaviour as #3's collision handling). No auto-suffix, no per-user namespace. Build this part with (a).
#9 short-link lifecycle — shipped (shift-1)
Expiry + max-views built end-to-end, verified live on
post.msbls.de. Duplicate policy: shipped default (a) first-come-first-served — flagged to m for a call (see below).Commit: https://mgit.msbls.de/m/postcards/commit/bd4ffde
Branch:
mai/hermes/issue-9-feat-short-link(otto-head merges + deploys)What it does
validUntilset to end-of-day UTC (23:59:59) of the chosen day — a deliberately generous boundary so the link never dies before the day is over anywhere. After it the pretty slug 404s.maxVisits. After N hits the slug 404s./c/{token}stays reachable forever (the durable artifact), exactly as the issue's default said.005_slug_lifecycle.sql, nullable — so it shows without calling back to Shlink).Duplicates — needs m's call (shipped (a), no block)
Shlink slugs are globally unique per domain and the app has no accounts, so a slug exists once ever. Shipped (a) = current #3 behaviour: a taken slug → „schon vergeben, bitte wähle eine andere“, zero new concepts, zero orphan cards. (b) auto-suffix (
oma-erika-2) is an easy follow-up if m prefers it; (c) per-sender namespaces is a bigger change. No code shipped for (b)/(c) pending m's decision — current behaviour is unchanged and safe either way.Validation (all before any side effect — bad input = clean retry, no orphan)
e2e (live
post.msbls.de, real Shlink, prod DB)maxVisits=1slug → 1st hit 302, 2nd hit 404 (exhausted) ✅validUntil→ 404 immediately ✅2026-12-31→validUntil=2026-12-31T23:59:59+00:00in Shlink ✅/to//from/(#8) unaffected, no-slug card works ✅slug_valid_until timestamptz,slug_max_visits int); all test slugs + DB rows cleaned up afterwards.DoD
validUntil/maxVisits)-racegreen; test data cleanedDeploy note for otto-head
No deploy change needed for the limits — Shlink enforces them and serves the 404 itself. For a friendlier expired/exhausted page than Shlink's bare 404, point Shlink's per-domain Invalid-short-URL redirect at a friendly URL (documented in
deploy/README.md§ 5). Optional polish; the DoD behaviour holds without it.One open question for m: confirm duplicate policy (a) (shipped) or switch to (b) auto-suffix / (c) namespaces.
✅ Merged & deployed — lifecycle live on sendmy.cards
Merged
bd4ffde(merge2365076), deployed. Migration 005 applied. Verified live e2e on sendmy.cards: amax_visits=1slug → 1st hit 302 → card, 2nd hit 404 (exhausted). Compose now has 'Gültig bis' (date) + 'Max. Aufrufe' (number); limits require a custom slug; the card's/c/{token}stays reachable forever. Test data cleaned.Duplicate policy: (a) first-come-first-served — matches m's decision (recorded above). Shipped. build/vet/test -race green.
Not closing — m closes.