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)