Skip to content

ADR-041: Share twin — generated shareable card

Status: Proposed Date: 2026-05-06 (revised after Cici clarification — see Discussion) Context: Cici's twin redesign includes a "Share twin" button. Initial draft of this ADR (v1, same date) designed a persistent live public profile page with signed tokens and revoke controls. Cici clarified via Slack on May 6 that the actual use case is Strava-style progress sharing — repeatedly shareable progress moments ("I trained my twin to 70% alignment"), not a live profile page. Live updating would remove reasons to share. This revision matches the actual product intent.

At a glance

What it decides: a proposal (not yet in force) to make "Share twin" generate a one-off static PNG card of the twin's current state, rather than a live public profile page.

  • Core mechanism — every tap produces a fresh image (name, sync_score, top drivers, sparkline, synapses) for the user to share anywhere; a moment-in-time snapshot, not a live link.
  • Main rejected alternative — the original v1 live public profile with signed tokens, revoke, and expiry (~10x complexity for a use case the users don't want).
  • Privacy — the card exposes only signature data the user chose by tapping; no decisions, rationales, or scenario titles ever appear.
  • Cost/risk — minimal: one analytics-only table (twin_share_events) and one auth'd API route; no public route to host, scrape, or revoke.
  • Open question — whether sharing actually drives signups; referral-link attribution is deferred until the share log shows correlation.

Decision

Implement share-twin as on-demand generation of a static shareable card (PNG image). No persistent public URL. No tokens. No expiry. No privacy controls. The card captures the twin's signature at the moment of generation; the user can share the image wherever they want.

Generation flow: 1. User taps "Share my twin" on the twin page. 2. Server generates a PNG via Next.js's image generation API, populated with the user's current state: name, sync_score, top 3 drivers, 14-day sparkline, synapses (per ADR-042 if accepted), avatar, "twin since" date. 3. Image is returned to the client as a blob. 4. Native-share sheet opens (mobile) or a download + copy-link affordance shows (desktop). 5. The image carries Beacon branding + a small CTA URL ("see your own twin at sync.beaconinfra.xyz").

Repeatedly shareable. Every press of "Share my twin" produces a fresh card reflecting current state. Users naturally generate new cards as their twin grows — that's the engagement loop Cici described.

flowchart LR
    subgraph card [On the card — shared]
        S[sync_score]
        DR[top 3 drivers]
        SP[14-day sparkline]
        SY[synapses]
    end
    subgraph priv [Never on the card — private]
        DEC[decisions]
        RAT[rationales]
        SC[scenario titles]
    end
    U[User taps Share] --> card
    card --> IMG[Static PNG] --> SHARE[Share anywhere]

Only signature data the user chose by tapping is rendered into the image; decision history stays private.

Schema:

twin_share_events (analytics only):
  id, user_id (FK), generated_at,
  sync_score_at_share, drivers_at_share (jsonb),
  share_method ('download' | 'native' | 'copy-link' | null)

That's the only schema change. No tokens, no public URL routes, no token-revoke API.

Rationale

Cici's three reasons-to-share, verbatim: - "Proud of having a well-aligned twin" - "It reveals positive traits about me that I want to share with other people / get attention for" - "Progress over time"

Each of these requires a moment-in-time snapshot, not a live URL. A live profile that updates after the recipient clicks the link breaks the third reason entirely (the recipient sees "current you," not "past you proud-moment").

The Strava analogy captures this exactly: Strava shares are screenshots of a specific run, not a live link to your training schedule. Cards beat profiles for this use case.

Static cards also collapse the privacy surface to nearly zero. The card doesn't expose decisions, rationales, scenarios — only signature data the user explicitly chose to share by hitting the button. There's no persistent surface to attack, scrape, or revoke.

Alternatives Considered

  • Live shared profile with tokens (the original v1 of this ADR). Rejected after Cici clarification. Would have required: token system, revoke/expiry controls, public route, opengraph card generation tied to live data. ~10x the complexity for a use case the actual users don't want. Filed as a possible Phase 2 if a different use case (e.g. inviting collaborators to a live team profile) materializes — but separate from share-twin.
  • Public username slug (/twin/jonathan). Rejected for the same reason: the recipient sees a live, current snapshot, not the moment the sender was proud of.
  • No share feature at all. Considered. Rejected because the engagement loop (pride + progress + traits revealed) is a real motivation flywheel for repeated play. Strava demonstrates this loop works at scale.
  • Show recent decisions on the card. Rejected. Even sanitized scenario titles can be sensitive ("Jonathan shared a card showing he chose X on the Whistleblower scenario" is exactly the context-collapse to avoid). Cards stay at signature, never history.
  • Embed the card in a social-style share-feed inside Sync. Rejected for v1. Out of scope; would change the product shape from "decision-twin tool" to "decision-twin social network."

Discussion

The original v1 of this ADR was wrong because I designed a different feature than Cici asked for. The literal source material in Cici's audit was "Generates a public read-only link. Needs a public twin-view route + a share table or signed URL system." I read "public read-only link" + "signed URL system" and built a live-profile architecture. Cici actually meant "shareable thing" — and the system they described in Slack ("Strava type humble brag... repeatedly... progress over time") is fundamentally a card, not a profile.

The asymmetry between the two designs is worth preserving as a reference:

Card (this ADR) Live profile (rejected)
What's shared An image at a moment in time A URL pointing to current state
When does the recipient see what the sharer wanted them to see? Immediately, forever Only if they click immediately
Privacy surface Nearly zero (image only) Persistent (live route, token, scraping risk)
Complexity Image generator + analytics row Token system + public route + OG cards + revoke + expiry + RLS
Maps to what motivates sharing? Yes (pride moment, progress) No (live state breaks "look at this snapshot")

The decision not to log raw shared images is deliberate — twin_share_events records the metadata of each share (when, what state) but not the rendered image bytes. We don't need to retain copies; the image is ephemeral by design.

One detail worth calling out: the CTA on the card is a generic "sync.beaconinfra.xyz" URL, not a referral link. Adding referral-link tracking is a useful follow-up (especially if share-twin is a meaningful acquisition channel) but adds complexity (signed referral codes, attribution logic) that isn't worth it before we know whether sharing actually drives signups. Watch the share-event log; if it correlates with new-user signup spikes, build referral attribution then.

Consequences

  • One new table: twin_share_events. Analytics only. No FK to any token system because there isn't one.
  • One new API route: POST /api/twin/share-card — auth'd, returns PNG bytes. Generates the card from the user's current twin state.
  • No new public route. The image is the share artifact; there's nothing to host publicly.
  • Card design is a Cici deliverable — needs the visual treatment locked before implementation. Card needs to render well at small sizes (Twitter card, iMessage thumbnail) and be unambiguously Beacon-branded so recipients can find their way back.
  • Watch: share-event rate per active user. If it's <0.1 events per user per week, the feature isn't earning attention surface; if it's >2 events per user per week, the card is a meaningful engagement loop and worth investing more in (referral attribution, share-history view, etc).
  • Watch: card-share → signup correlation. Crude version: count new signups in the 24h after a share event from a user with high share rates. If the correlation is real, ship referral attribution.

Key files (proposed): - migrations/0XX_twin_share_events.sql — schema - src/app/api/twin/share-card/route.ts — image generation endpoint (Next.js OG-image-style) - src/components/twin/ShareTwinButton.tsx — UI surface, native-share sheet integration - src/lib/twin/render-share-card.tsx — JSX-based image render (matches Next.js ImageResponse API)