Highlighted Stories

Editorial content surface — Instagram-style mobile stories and desktop article cards used for announcements, feature launches, and promos.

What this is

Highlighted Stories is the editorial surface used for announcements, feature launches, and promos. It has two manifestations:

  • Story Modal — mobile-first, Instagram-style. Tap zones to advance, 5-second auto-advance, emoji reactions.
  • Highlight Modal — desktop-first, article-reader. Long-form body with sanitised HTML, supports rich blocks (headings, blockquotes, code).

Stories are surfaced from the app home as a horizontal shelf of cards on the welcome screen, plus a dedicated circular “Story” entry that opens the mobile viewer.

Data model

Stories are stored as JSONB in the highlights table. Localised fields are stored as LangMap objects (Record<string, string>) so a single story carries all 7 locales.

type HighlightRecord = {
  id: string;
  position: number;
  active: boolean;
  expires: boolean | string;     // true = 24h auto-expire from createdAt; ISO timestamp = explicit expiry
  tag: string;                   // 'promo' | 'new_feature' | …
  title: LangMap;
  description: LangMap;
  body: LangMap;
  coverImage: LangMap;
  mobileAvatar: LangMap;
  ctaLabel: LangMap;
  ctaLink: string;               // 'https://…' or 'app://chat' / 'app://trade' / …
  prompt?: string;               // optional text to prefill chat input on app://chat CTAs
  coverMediaType: 'image' | 'video';
  innerMediaType: 'image' | 'video';
};

A separate story_reactions table records (user_id, story_id, reaction_type) with a composite primary key.

Editorial rules

  • Maximum 20 highlights in the system at any time. Creation is rejected past the cap.
  • Title required. Body and cover are optional but strongly encouraged.
  • HTML sanitisation. Body content is run through an allow-list (HIGHLIGHT_HTML_CONFIG) that permits h1h6, p, blockquote, code, lists, and basic inline tags. Anything else is stripped.
  • CTA targets. External CTAs must be http(s). Internal CTAs use the app:// scheme. Recognised internal targets include app://chat, app://trade, app://stake, app://signals, app://agents.
  • Prompt prefill. A story carrying prompt and a CTA pointing to app://chat prefills the chat input with the prompt; it does not auto-send.
  • Position is editorially controlled and re-orderable from the admin tab.

Visibility

A story is “live” when:

  • active === true, and
  • If expires is an ISO timestamp, that timestamp is in the future; if expires === true, less than 24h has passed since createdAt; otherwise no expiry applies.

Live stories are returned by GET /v1/content?lang={locale} and cached stale-while-revalidate at the v1-content:{lang} key for 5 minutes.

Media handling

  • Storage. Uploads are routed through ObjectStorageService and persisted at /api/storage/public-objects/highlights/…. Legacy /api/uploads/ paths are auto-migrated on server boot.
  • Allowed formats. Images: JPG, PNG, GIF, WebP. Videos: MP4, WebM, MOV.
  • Safe URL guard. isSafeMediaUrlOrPath rejects anything that isn’t HTTPS or a verified internal storage path.
  • Video behaviour. Cover videos autoplay muted in the shelf; the story viewer freezes on the last frame instead of looping.
For Partners

The editorial surface is staff-controlled. Partners do not currently publish stories directly; integration requests for partner-tagged stories go through the Partner Integration channel.

For Developers

Public endpoints

GET /v1/content

Query: lang. Returns the live story list merged with general app content. Cached 5 minutes per locale.

POST /v1/highlights/{id}/view

Increments the view count.

GET /v1/highlights/{id}/reactions

Returns reaction counts and the current user’s reactions.

POST /v1/highlights/{id}/reactions
DELETE /v1/highlights/{id}/reactions

Add or remove a reaction (heart, fire, …).

Admin endpoints

GET /v1/admin/highlights

Lists all stories, including inactive and expired.

POST /v1/admin/highlights

Create a new story. Validates the 20-story cap and the HTML allow-list.

PUT /v1/admin/highlights/{id}

Update fields. LangMap fields are merged, so partial updates are allowed.

PUT /v1/admin/highlights/{id}/toggle
PUT /v1/admin/highlights/{id}/reorder
DELETE /v1/admin/highlights/{id}

Safety, limits, failure modes

  • HTML allow-list is strict. Custom embeds and scripts are stripped silently. Author copy in plain HTML or use the AI formatting helper in admin.
  • Cache TTL. Updates take up to 5 minutes to surface in the public list because of the per-locale content cache.
  • Cap enforcement. Hitting 20 stories blocks new creates; archive or delete older stories first.
  • Locale fallback. If a story’s LangMap is missing the requested locale, the platform falls back to the English entry.

See also

Last updated: