# Showcase links

How a public showcase of one car or the garage is built from the passport's machinery, how Live mode keeps it current, and how the page at /g/:token shows it without plate, VIN or prices.

## What it does [#what-it-does]

A Showcase ([social-sharing spec §2](https://github.com/CarlosZiegler/autofolio/blob/main/docs/specs/social-sharing.md)) is for showing cars, where a [Passport](/docs/features/passport) is for selling one. It is a public page of one car or of cars of the garage the owner picks, sold ones included (never a wished-for or archived car). It always shows make, model, year, the years with each car and the owner's first name; the owner adds photos and milestones (on by default), distance and the full name (off). Prices, plate, VIN, address, documents and receipts are never offered, and `materializePassport` from `@autofolio/domain` throws if a forbidden field is asked for.

It reuses the passport's machinery rather than a copy of it: the 256-bit token, its SHA-256 as the only stored form and its eight-character hint (`passport.rules.ts`), photo copies under the link's own prefix (`showcases/<id>/<random>.jpg`), the public page's rate limit (`consumeRateLimit`, 60 openings a minute per address) and one neutral answer for revoked, expired and unknown links. Creating one writes a ★ `consent_record` (`showcase_share`, version `showcase-2026-10`).

## The table [#the-table]

`showcase` holds the scope (`vehicle` or `garage`), the cars' ids in the owner's order, the fields, `live`, the snapshot, a view counter, an optional expiry and `revoked_at`. Row-level security shows a link to the Member who made it and to the household's owners and admins; a Member inserts one only for cars they may see, and the procedure also needs the `owner` role on every car (`requireVehicleRole("owner", "vehicleIds")`). Tenant traffic may change `live`, revoke (which drops the snapshot) and rotate a replayed create's token; views and the live refresh are system paths. CHECKs refuse plate, VIN and address keys in the snapshot and a revoked row that keeps one.

The snapshot (`ShowcaseSnapshot` in `showcase.rules.ts`) stores each car's Ownership Periods, so the page counts whole years to today or the sale, and its milestones: the day it joined, every whole year with it and its sale.

## Live mode [#live-mode]

Off, a link stays as it was created. On, the cron tick (`pendingShowcases` in `showcase.jobs.ts`) rebuilds it at most every 15 minutes from the cars as they stand: a new gallery photo is copied in, a deleted one's copy is removed, new milestones appear. A car that left the household drops out. The same job removes an expired link's photo copies and snapshot; the row stays "expired".

## The public page [#the-public-page]

`/g/:token` in `apps/web` (Social-Public) needs no account. Its loader calls `showcase.open { token }`, a public procedure on the system path (SystemReason `showcase.open`) that counts the view and answers the view or `null`. It shows "Showcase shared by" the owner's name, the garage in numbers, a card per car with its photos, years, distance when shared and milestones, the privacy line and "Start your garage". It speaks the visitor's language among pt, en and de, else the owner's. It is black and white unless the owner turned on "Colour on the showcase too" ([Appearance](/docs/features/appearance), `account_preference.showcase_color`, ADR-0016): then each car in the view carries its cover's colour, kept in the snapshot, and the page takes the first one that tints. Like `/p/`, responses under `/g/` carry `X-Robots-Tag: noindex`, `Cache-Control: private, no-store` and `Referrer-Policy: no-referrer`, `robots.txt` disallows the path and the log scrubber redacts the token.

## Procedures [#procedures]

| Procedure                                                          | Failures                                                                                                         |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `showcase.create` `{ id?, scope, vehicleIds, fields, live, days }` | `INPUT_VALIDATION`, `FORBIDDEN` (not owner of a car), `NOT_FOUND` (car), `CONFLICT` (wished-for or archived car) |
| `showcase.list` `{ limit? }`                                       | `INPUT_VALIDATION`                                                                                               |
| `showcase.setLive` `{ id, live }`                                  | `NOT_FOUND` (link), `CONFLICT` (revoked or expired)                                                              |
| `showcase.revoke` `{ id }`                                         | `NOT_FOUND` (link)                                                                                               |
| `showcase.open` `{ token }`                                        | `INPUT_VALIDATION`, `TOO_MANY_REQUESTS`                                                                          |

## In the app [#in-the-app]

Behind the `social.showcase` flag (off by default; the dev seed turns it on): M-Colecao's share button (its "Public link" choice when `social.moments` is on too) and Profile open Showcase links (Social-Showcase, `autofolio://dev/Social-Showcase`), with each link's views, Live switch and Revoke, and "New showcase link". M-Veiculo's more sheet shares one car. The link is shown once, in the passport's Link created sheet.

