Announcements
Site news replaces the weekly Discord export → news.json pipeline. Abstract Play is the source of truth; Discord #announcements is mirrored on first publish only.
Front plan: announcements fan-out architecture (living plan in maintainer docs). Phase 0 locked 2026-09-16.
DynamoDB
Single-table keys (stage table abstract-play-dev / abstract-play-prod):
| Item | pk | sk | Notes |
|---|---|---|---|
| Announcement | ANNOUNCEMENT |
<id> |
id = UUID for new posts; Discord import uses snowflake message.id |
| Published index (GSI) | ANNOUNCEMENT_PUBLISHED |
<publishedAt>#<id> |
List published, newest first; project metadata + reactionCounts |
Announcement fields (core):
status:draft|scheduled|published|retractedtitle,body(markdown),attachmentKeys[]publishedAt(ms),createdAt,updatedAt, optionaleditedAtsource:ap|import-discorddiscordMessageId(after webhook post)reactionCounts:Record<emoji, number>(denormalized)
Reactions (minimal v1):
- pk:
ANNOUNCEMENT, sk:REACTION#<emoji>#<userId> - Toggle via
announcement_react; allowlisted emoji only - No per-user notification rows on react
User read cursor (on USER settings, not a separate item):
settings.all.announcementsLastReadAt(epoch ms)- Unread = published items with
publishedAt> cursor
API queries
| Query | Auth | Role |
|---|---|---|
announcements_list |
Public | Published only; pagination; includes reactionCounts |
announcement_get |
Public | By id; attachmentUrls for keys |
announcements_admin_list |
Admin | All statuses |
announcement_save |
Admin | Draft create/update; edit published (no fan-out) |
announcement_presign_upload |
Admin | Image upload for drafts/edits |
announcement_publish |
Admin | First-publish fan-out only |
announcements_mark_read |
Auth | Bump announcementsLastReadAt |
announcement_react |
Auth | Toggle allowlisted emoji |
announcement_reactions_mine |
Auth | { ids: string[] } → user's emojis per id |
Register handlers in lib/announcements/authHandlers.ts alongside feedback patterns.
Unread and in-app bell (front)
- No Dynamo notification rows on publish (avoid N×users writes).
- Front merges synthetic unread rows in the navbar bell from
announcements_list+ server cursor. - Hidden when
settings.all.inAppNotifications.announcements === false(default on).
Notification prefs
| Channel | Setting key | Default | When |
|---|---|---|---|
| In-app bell rows | inAppNotifications.announcements |
on | Synthetic list only |
notifications.announcements (email map) |
off | First announcement_publish only; recipients from ANNOUNCEMENT_NOTIFY index (synced on update_user_settings) |
|
| Push | notifications.announcements (same email map) |
off | First announcement_publish only |
Edits to published posts do not re-send email/push or reset unread cursor.
Dev vs prod
| Stage | announcement_save / presign |
announcement_publish |
|---|---|---|
prod (WEBSOCKET_STAGE=prod) |
Admin allowed | Admin allowed |
dev (WEBSOCKET_STAGE=dev) |
Admin allowed (draft/composer testing) | 403 announcements_publish_disabled_on_dev |
Front play.dev: disable Publish in admin UI (VITE_REAL_MODE=development). Ops import CLI is not gated.
Publish vs edit
| Target | announcement_publish (first time) |
announcement_save on published |
|---|---|---|
| Site list/get | Visible | Immediate update |
Static news.rss |
Regenerate + upload | Regenerate (same <guid>) |
| Discord webhook | POST once; store discordMessageId |
No auto-update |
| Email / push | If pref on | No |
| Unread cursor | Users see new items | No re-notify |
Retract: status: retracted; hide from list/RSS; Discord follow-up manual or bot later.
Discord webhook (v1)
Env: ANNOUNCEMENTS_DISCORD_WEBHOOK_URL (secrets manager per stage).
Example payload:
{
"content": "**{title}**\n{excerpt}\nhttps://play.abstractplay.com/news/{id}"
}
Excerpt = plain-text first ~200 chars of body, unless the body contains an excerpt marker <!--ap:discord-excerpt-end--> (HTML comment in markdown). With the marker, Discord uses plain text before the marker and always appends …; text after the marker is site-only. The marker is stripped for /news, RSS, and preview. Full markdown and images live on site only.
Attachments
- S3 prefix:
announcements/{announcementId}/…(bucket TBD per stage; separate from feedback attachments). - Markdown:
in body; resolve to presigned GET on read. - Import:
npm run import-discord-announcementsuploads from DiscordChatExporterattachments[]only (not avatars/reaction SVGs).
Discord history import
- Canonical local export:
announcements.json+ siblingannouncements.json_Files. - 405 rows: non-empty
contentand/or message attachments; skip embed-only Builderman bot cards (58). - 68 attachment files across 51 messages.
- Inline emojis: keep Unicode in
content; rewrite<:name:id>tokens usinginlineEmojis[].namewhen present.
Reactions (site v1)
- Curated palette (~8); counts on list from
reactionCounts. announcement_reactions_mineonce per/newsload for highlight state.- Do not import Discord
reactions[]from export.
Related
- Notifications — email/push delivery
- Public queries —
announcements_list,announcement_get - Auth queries — admin save/publish, mark_read, react