# TL · Tutelink, short links (build plan step 5)

**Status:** TL-1 to TL-12 decided under the Architect's autonomy grant, 9 November 2026. Mockups (`reji/ui/html/tl/`: link, links) drawn
and checked in light, dark and on a phone. TL1 to TL3 built, 9 November 2026.

---

## 0 · The one-paragraph version

Tutelink turns a product's long link into `tutel.ink/k7m2xq4`, so a fee reminder's link fits in a 160-character text and looks like it
came from someone the parent knows. A product's server asks for one (or Nuntius shortens a text's links itself, where the client has short
links on); a tap redirects, counting the tap and nothing about who tapped. Only system links: the product's own domain, the
organisation's verified addresses for that product, and Atrium's own hosts. A link expires (a year unless the product says sooner, a
report card's in 30 days) and can be turned off at once. `tutel.ink` does nothing else: no sign-in, no API, no "shorten any URL".

**What it is.** Short, trustworthy links for every product's messages, and a count of taps.
**Scope.** In Atrium: making a link for a product's server, redirecting, counting taps, expiry and turning off, the page a person sees when
a link doesn't work, Nuntius shortening texts' links, the console's list. Staying in the products: which links to send and to whom.
Out of TL: a neutral short domain for outside clients (trigger: the first outside client that sends links), and per-recipient links.
**Failsafe.** Atrium down: a tap fails, but the text's words still say what it was about, and products can send the full link (the
spec's fallback, at the cost of a longer text). `tutel.ink` blocked by a network: only links are lost; sign-ins and APIs live elsewhere.
A link sent by mistake: turned off in a second, by the product or the Service.

---

## 1 · What the apps already do

Read 9 November 2026. **Nothing, anywhere:** no app shortens links (Campus, Desk and Loop send full URLs). The foundation already gives
`tutel.ink` its own host (`HOST_INK`), answering every path with the "this link doesn't work" page and nothing else, and Nuntius has the
setting `notify.short_links` (NS-11), off by default with a client able to differ, doing nothing until now.

## 2 · Decisions

**Decided under the Architect's autonomy grant, 9 November 2026.**

| # | Decision | Chosen | Why, and the alternative |
|---|---|---|---|
| **TL-1** | Who makes a link | A product's server with the `links` scope (`POST /v1/links`), for the organisation it names; and Nuntius, for a text's links. No public shortener, no page at id to make one | The spec: system links only, which keeps the domain off blocklists |
| **TL-2** | Where a link may go | `https` only, to the calling product's own domain and its subdomains, that organisation's verified addresses for that product, or Atrium's own hosts. Anything else is refused (422). A server with no product (an outside client) gets no `tutel.ink` link | The spec's "Tutelora domains and verified tenant domains", narrowed to the caller's own, so one product can't point a link at another's pages |
| **TL-3** | The code | 7 characters from 31 (lower-case letters and digits with no look-alikes: no 0, o, 1, i, l), random, unique; read case-insensitively. About 27 billion codes. A code is never reused: a purged link keeps its code | Shorter codes run out or can be walked; mixed case is misread aloud |
| **TL-4** | Expiry | Every link expires: the product may say when (1 day to 5 years); otherwise `links.default_days`, a setting (365, a client able to differ). Never "for ever" | The spec's forwarded report card; a link that never expires is a leak waiting |
| **TL-5** | Turning off | At once, by the product (`DELETE /v1/links/{code}`) or the Service on the console with a reason. Audited | The spec |
| **TL-6** | What's counted | Taps: a count a link and a count a day, nothing about who (no address, no device, no cookie). Link previews (WhatsApp, iMessage and the like, by their known agents) and `HEAD` aren't taps | The spec's "without any tracking beyond the click count" |
| **TL-7** | The redirect | `302` with `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, `X-Robots-Tag: noindex`, so turning a link off works even for someone who tapped it before; nothing in the short link is passed on | A `301` is cached by browsers for ever |
| **TL-8** | When a link doesn't work | One page, lightweight, on a phone first: never existed, expired, turned off. It names who sent it (the organisation and product) when known, never where it went, and says to ask them for a new one | The spec's "the one screen almost every user will see" |
| **TL-9** | Nuntius shortening | Where `notify.short_links` is on, a text's `https` links that TL-2 allows are shortened before its segments are counted, only when that makes the text shorter; one link per distinct address per send (a bulk send's 600 texts share one); codes never | The spec's offer letter. Shortening every link would fail on links TL-2 refuses |
| **TL-10** | Keeping the destination | Encrypted at rest (it may carry a signed token), with its host kept plainly for the console's search. A link 180 days past its end has its destination emptied (`links.keep_days`), keeping its code and counts | A database copy shouldn't hand out offer letters |
| **TL-11** | Who sees links | The console (Links: search by code or host, state, taps by day, turn off). The product reads its own links' counts (`GET /v1/links/{code}`) | An organisation's own page waits until a product needs one |
| **TL-12** | Outside clients | No `tutel.ink`; a neutral domain when the first outside client sends links | The spec |

## 3 · Mockups (`reji/ui/html/tl/`)

- **`link.html`**, on `tutel.ink`, phone first: the link doesn't exist, has expired, was turned off (each naming who sent it when known,
  never where it went); a desktop view.
- **`links.html`** in the console: the list (search, state, taps), one link (its taps by day as a small chart, where it goes by host, its
  end, turning it off with a reason).

## 4 · Slices

| Slice | What | Done when |
|---|---|---|
| **TL0** ☑ | Mockups | Drawn and checked in light, dark and on a phone |
| **TL1** ☑ | The link: table, code, the API to make, read and turn off, the redirect on `tutel.ink` with its counting, the page when a link doesn't work, expiry, the purge of old destinations | A link made, tapped, counted (a preview not counted), expired and turned off, by test; a destination outside TL-2 refused |
| **TL2** ☑ | Nuntius shortening a text's links | A text with a long allowed link sent shorter, a refused link left as it is, one link for a whole bulk send |
| **TL3** ☑ | The console's Links | Search, taps by day, turning off with a reason, by test and in the app |

## 5 · Open items for the Architect

- `tutel.ink`'s DNS and certificate pointed at Atrium (`HOST_INK=tutel.ink`) before a link is sent for real.

### 5.1 · What building added

- **TL1 to TL3 (9 November 2026).** The short-link host is answered from `EnforceHost`, before any session, so it sets no cookie. A link
  that doesn't work answers 404 (never existed) or 410 (expired, turned off). A product's own `DELETE` raises nothing; the Service's turning
  off tells the product's server (`links.turned_off`). Links made for a text are counted like any other (`usage_event`), and one made for a
  bulk send that is then refused stays, harmlessly, until it ends. A link's taps are a count; nothing of who tapped exists to show.
