Message Beacons¶
Status: š§ Partial ā beacon deploy/read kernel partially shipped; full player-facing beacon UX incomplete. (re-verified 2026-08-21 vs Sectorwars2102
46bce720.)
A small physical object a player can deploy in any sector. The beacon carries an arbitrary text message and an author identity, sits in space until salvaged or self-destructed, and can be read by any other player who arrives in the sector. Used for emergent storytelling, route-marking, warning fellow travelers about hazards, leaving graffiti on the universe, and giving players a way to communicate asynchronously across time without using the persistent messaging system.
This is the "message in a bottle" mechanic ā orthogonal to direct player-to-player messaging (the persistent inbox / Message model documented in DATA_MODELS/player.md) and to the realtime chat bus. Beacons are physical objects in the world, discoverable by traversal, not by directory lookup.
Purpose¶
Message beacons fill three gaps:
- Asynchronous in-world communication. A player can leave a message at sector 47 saying "the desert planet here is mine, don't bother colonizing" without needing the recipient's player ID ā the next person who arrives reads it.
- Route-marking and trail-blazing. "There's a Lumen Crystal anomaly two sectors north" left at a junction tells later explorers what's worth their time.
- Emergent worldbuilding. Players leaving graffiti at famous battle sites, the Nexus Capital, formation interiors, etc. The universe accumulates player-authored history that newer players discover organically.
How they work¶
Deployment¶
Any player ship that can carry cargo deploys a beacon by spending 5 turns + 500 credits + 1 inventory slot of equipment cargo. The beacon is configured with:
- A message (1ā500 characters of text; standard player-input sanitation per the AI-anti-griefing layer at
../../OPERATIONS/aria.md). - An optional read-once flag. When set, the first reader's act of reading destroys the beacon. Useful for treasure hints and dead-drops. Default: false.
There is no player-facing expiry-choice menu (24h/7d/30d/never) ā that earlier design is retired (WO-BEACON-LIFECYCLE). Every deploy creates one fixed 30-day charge cell instead; see Lifecycle below for how it's extended or lapses.
Deployment commits via POST /api/v1/beacons/deploy with {sector_id, message, read_once} (no expiry field ā WO-BEACON-LIFECYCLE, see above). The server:
1. Validates the player has the resources, is in the sector, is not docked.
2. Validates the message text against the AI-anti-griefing filters (XSS, profanity blocklist, prompt injection prevention).
3. Inserts a MessageBeacon row.
4. Adds a beacon entry to Sector.message_beacons JSONB.
5. Emits a beacon_deployed realtime event to the sector's room so other players currently in the sector see it appear.
Discovery¶
Any player arriving in a sector with active beacons sees them in the sector view. The presence is broadcast via the realtime bus (sector.beacons_present payload on the sector-arrival event).
Sector-view popup (tip 46bce720): š§ preview-only ā windshieldTableauPopupContent.tsx case 'beacon' shows title, deployer nickname, meta.preview quote, and date. There are no Read/Salvage CTAs on that popup (PC LEG-481/#492).
Gameserver routes remain; player reachability is split:
- Read the beacon (
GET /api/v1/beacons/{id}/read) ā costs 0 turns. The full message and author identity (player nickname or username) become visible. Ifread_once = true, the beacon row is deleted on this call. ā Reachable from the My Beacons dossier tab (MyBeaconsTab.tsx); not from the sector popup until #492. - Salvage the beacon (
POST /api/v1/beacons/{id}/salvage) ā costs 1 turn. The beacon is removed; the salvager recovers 250 credits (50% of the deploy cost) but noequipmentcargo (the equipment is destroyed with the beacon's casing). ā My Beacons tab; not sector popup. - Ignore the beacon ā it stays in place, visible to other arrivals.
- Recharge the beacon (
POST /api/v1/beacons/{id}/recharge, 200 credits) ā extends the charge cell by another 30 days. Presence in the sector is not strictly required: the owner may recharge remotely, or another player physically in the sector may top it up on the owner's behalf.
A player cannot edit a beacon after deployment. To change the message, they must salvage their own beacon and redeploy a new one (full deploy cost).
Lifecycle¶
Beacons exist until one of:
- Salvaged ā by any player (the deployer included). Removes the row.
- Read with
read_once = trueā first read destroys it. - Charge cell lapses ā each deploy starts a fixed 30-day charge cell; a beacon can be recharged (200 credits, extends by another 30 days, stacking indefinitely ā 30d ā 60d ā 90dā¦) any time before it lapses. If the charge cell isn't recharged, the beacon survives a further 7-day grace period past the charge deadline, then auto-removes via the periodic beacon-expiry tick. The deployer is not notified; the beacon is just gone.
- Sector destroyed / region terminated ā CASCADE delete with the sector / region row.
Per-sector visibility cap (per ADR-0056 N-V2): 10 beacons visible per sector at any time. Once at cap, the next deployment auto-displaces the oldest beacon (FIFO); the displaced beacon is hard-deleted. Region operators may raise the cap up to 50 (MAX_SECTOR_CAP) for dense-traffic regions via Region.trade_bonuses['beacon_sector_cap'] (DECISION message-beacon-sector-cap-admin-route, 2026-08-07). ā
Admin REST setter tip-shipped on feat/new-feature-development 46bce720 / PR #766 ā GET/PATCH /api/v1/admin/regions/{id}/beacon-sector-cap (admin.py, clamp 1..50). š§ Admin UI editor Soft-HOLD until PR #764 / LEG-1149 / Fibril #1219 tip-lands (BeaconSectorCapEditor absent on tip). No player-facing route to set the cap. Default 10 closes the spam vector.
Schema¶
MessageBeacon¶
Source: services/gameserver/src/models/message_beacon.py (ā
Shipped).
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| region_id | UUID FK regions.id | not null, CASCADE | Region containing the sector. |
| sector_id | Integer | not null | The sector where the beacon sits; compound (region_id, sector_id) per the canonical sector identity. |
| deployer_player_id | UUID FK players.id | not null | The author. |
| deployer_nickname_at_deploy | String(50) | not null | Snapshot of the deployer's nickname at deploy time, so the message survives the deployer renaming or going inactive. |
| message | String(500) | not null | The text. Up to 500 characters; multi-line allowed. Sanitized at deploy time. |
| expiry | DateTime | nullable | REPURPOSED (WO-BEACON-LIFECYCLE) ā the hard-delete deadline only, always charge_expires_at + GRACE_PERIOD once a beacon has ever been charged; the earlier "player-facing expiry-choice menu, NULL = never expires" behavior is retired. NULL only for a legacy pre-migration row (should not exist post-migration). |
| read_once | Boolean | default false | If true, first read destroys the beacon. |
| read_count | Integer | default 0 | How many times the beacon has been read. Updated atomically on each read; visible to the deployer in their beacon-management UI. |
| deployed_at | DateTime | not null | Timestamp. |
| last_read_at | DateTime | nullable | Updated on each read. |
Indexes:
- (region_id, sector_id) ā the dominant query: "what beacons are in this sector?"
- (deployer_player_id, deployed_at DESC) ā deployer's beacon-management UI.
- (expiry) partial WHERE expiry IS NOT NULL ā the periodic expiry tick scans this.
Relationships:
- region ā Region (FK).
- deployer ā Player (FK).
Sector schema extension¶
Sector.message_beacons (ā
Shipped) ā JSONB array of beacon summaries denormalized for fast sector-view reads. Rebuilt from the live MessageBeacon rows on every deploy / salvage / read-once-read / expiry / sector-cap displacement, under a per-sector advisory lock. Shape:
[
{
"id": "<uuid>",
"deployer_nickname": "<str>",
"deployed_at": "<iso8601>",
"preview": "<first 60 chars of message>",
"expiry": "<iso8601 | null>"
}
]
Players read the JSONB array for a quick sector-arrival summary; they fetch the full message body via the MessageBeacon row when they actually read.
Anti-griefing¶
Beacon text passes through the same content-policy / anti-abuse layer as ARIA-mediated player input (../../OPERATIONS/aria.md):
- Length capped at 500 characters.
- XSS defense is encode-at-output, not sanitize-at-storage: beacon text is stored raw (canonical, unescaped) and every consumer encodes for its output context ā the player-client renders it through React's automatic escaping; any non-React or
innerHTML-based consumer (a moderator tool, an admin view) must escape at render. The deploy endpoint does not HTML-sanitize the stored value. (Input-side,validate_inputrejects prompt-injection / control-character obfuscation; the ASCII</>XSS delimiters are caught there and neutralized again at output.) - Profanity blocklist (configurable wordlist per region ā Federation Zone is stricter; Frontier Zone permits saltier language).
- Prompt-injection / jailbreak detection (since beacons can theoretically be read by a player whose ARIA is summarizing or translating; the AI-security service flags suspicious patterns).
- A per-player rate limit: 5 beacon deploys per UTC day ā prevents beacon-spam griefing without constraining genuine use.
- Personal-rep gate (per ADR-0056 N-V2): placement requires
personal_rep ā„ neutral(not Wanted, not deeply negative; threshold per./ranking.md). Existing beacons by accounts that subsequently go negative remain visible until they expire or are displaced. - Multi-account discount (per ADR-0056 E-V5): free-tier accounts in a flagged cluster have beacon weight 0Ć ā their beacons don't count toward the per-sector cap and aren't surfaced in the sector-view list. Paid-tier accounts unaffected. Detection lives in
../../OPERATIONS/multi-account-detection.md. - ā
Shipped ā a per-player trust score read (
Player.aria_trust_score, ARIA trust model) that auto-flags very-low-trust beacons for moderator review before becoming player-visible (deploy()setsflaggedwhen trust is belowTRUST_AUTOFLAG_THRESHOLD0.2 [NO-CANON],34d5cd7a). Flagged cells are excluded from sector denorm until adminclear_flag.
ā
Shipped. Reports against beacons (POST report endpoint) flag the beacon for moderation; a flagged beacon is hidden from the sector-view list and read endpoint (anti-oracle 404, not a leaked "flagged" state) via a flagged bool + hide denorm/read path in message_beacon_service.py. Admin clear/review (clear_flag, list_flagged_beacons) is also shipped, giving admins a reversible unflag path. Trust-score auto-flag on deploy is also ā
shipped (above). ā
Shipped ā admin confirm_abuse docks the deployer's aria_trust_score by TRUST_DOCK_CONFIRMED_ABUSE 0.1 [NO-CANON ā same class as ARIA rate-limit / inappropriate-content default], increments aria_violation_count, and removes the beacon row (POST /admin/beacons/{id}/confirm-abuse). Does not auto time-ban / suspend ā that remains Max-gated.
Cross-region behavior¶
A beacon is regional. Its region_id ties it to one region; it cannot be discovered from another region even if a player traverses through the Nexus. A player who wants to communicate cross-region uses the persistent messaging system (see ./messaging.md ā š§ Partial overall, with send / inbox / conversations / mark-read / soft-delete shipped) or just leaves duplicate beacons in multiple regions.
When a region is terminated (per the subscription-lapse ā 30-day flow in ../../OPERATIONS/monetization.md), all beacons in the region are deleted via CASCADE. Deployers are not migrated or refunded ā beacons in a dying region are part of the history that goes down with it.
Player UX¶
ā
My Beacons dossier (1bc7540d, MyBeaconsTab.tsx) ā deploy / read / salvage / recharge / report as listed below. š§ Sector-view popup is preview-only on origin/feat 46bce720 (windshieldTableauPopupContent.tsx beacon case) ā no Read/Salvage CTAs (PC LEG-481/#492).
ā
Shipped (1bc7540d) ā sector-map beacon presence via BeaconLayer (windshieldTableauChrome.tsx / WindshieldTableau.tsx, always-visible per canon, not scan-gated).
- Sector view shows beacon presence. A small icon (per the player-client UI per
../../OPERATIONS/player-client.md) indicates "N beacons here" with the count. - Click the icon to expand a preview ā sender, deploy time,
meta.preview. š§ Not a Read/Salvage surface on tip. - ā My Beacons tab lists beacons the player has deployed, with read / salvage / recharge / report actions (same-sector required for read/salvage/report).
- ā Deploy from the My Beacons dossier tab ā costs 5 turns + 500 credits + 1 equipment (current sector).
- ā Read / Salvage from that tab ā Read costs 0 turns (full message); Salvage costs 1 turn, refunds 250 credits.
Still š design-only (unchanged): auto time-ban / suspension on confirmed abuse ā see Status.
Failure modes¶
| Mode | Detection | Recovery |
|---|---|---|
| Beacon-spam griefing | Per-player rate limit + per-sector cap | Reject with rate-limit error |
| Profanity / abuse | Content-policy filter at deploy (shipped); report flow at read (ā shipped); admin confirm-abuse (ā shipped) | Auto-reject deploy; flag-and-hide on report; admin clear-flag reverses a false report; confirm-abuse docks deployer trust + removes beacon |
| Beacon survives sector deletion | CASCADE FK | Beacon is deleted automatically |
| Deployer goes inactive | deployer_nickname_at_deploy snapshot |
Beacon survives; nickname display is the snapshot, not a live lookup |
Sector.message_beacons JSONB drifts from MessageBeacon rows |
Periodic invariant check | Reconcile from rows; rebuild JSONB |
Player-facing affordances¶
- ā
My Beacons dossier ā
MyBeaconsTab.tsxRead / Salvage / Deploy / Recharge / Report (StatusBar beacons tab; origin/feat46bce720). - ā
Sector-map beacon presence ā
BeaconLayerinwindshieldTableauChrome.tsx, mounted fromWindshieldTableau.tsx; always visible, not scan-gated. - š§ Sector popup preview-only ā
windshieldTableauPopupContent.tsxcase'beacon'shows title, deployer, preview quote, date; no Read/Salvage CTAs until PC LEG-481/#492 lands. - š Auto time-ban on confirmed abuse ā admin
confirm_abusedocks trust and removes beacon; no auto suspend/time-ban (Max-gated).
Status¶
š§ Partial. The gameserver kernel is ā shipped:
MessageBeaconmodel,message_beacon_service.py(deploy / read / salvage / expiry sweep),routes/beacons.py, theSector.message_beaconsJSONB denorm, the read-once flag, the per-sector FIFO cap, and every anti-griefing gate described above ā rate limit, personal-rep gate, multi-account discount, encode-at-output XSS defense, and trust-score auto-flag on deploy (34d5cd7a). Report + admin-clear are also shipped (player-facing report endpoint, flag-and-hide, adminclear_flag/list_flagged_beacons). Admin confirm-abuse deployer consequences also shipped (confirm_abuse: trust dock + violation bump + row delete; no auto-ban). ā Admin region beacon-sector-cap REST tip-shipped on46bce720/ PR #766 (GET/PATCH /api/v1/admin/regions/{id}/beacon-sector-cap). š§ Admin UI BeaconSectorCapEditor Soft-HOLD until PR #764 / LEG-1149 / Fibril#1219tip-lands (grep empty on tip admin-ui). Player-client split on origin/feat46bce720: ā My Beacons dossier (MyBeaconsTab.tsx,1bc7540d) wires Read/Salvage/Deploy; ā sector-map presence viaBeaconLayer(windshieldTableauChrome.tsx, always-visible per canon, not scan-gated); š§ sector-view popup (windshieldTableauPopupContent.tsxcase'beacon') is preview-only ā no Read/Salvage CTAs (PC LEG-481/#492). Still š design-only: auto time-ban / suspension on confirmed abuse (Max-gated). The anti-griefing layer reuses the existing ARIA content-policy filters; the realtime broadcast reuses the existing bus rooms. (Re-verified 2026-08-21 vs Sectorwars210246bce720ā LEG-1126 admin REST honesty; Admin UI Soft-HOLD.)
Cross-references¶
../../OPERATIONS/aria.mdā content-policy filters that beacon text passes through../bounties.mdā parallel "leave-something-in-the-universe" mechanic; beacons are the message-only counterpart.../../SYSTEMS/sector-presence.mdā sector-arrival event flow that broadcasts beacon presence.../../SYSTEMS/realtime-bus.mdā bus events forbeacon_deployed/beacon_salvaged/beacon_expired.../../OPERATIONS/player-client.mdā UI surfaces for beacon presence and deploy.../../DATA_MODELS/galaxy.mdāSectorschema extended with themessage_beaconsJSONB column.