Message Beacons¶
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 expiry timer (24 hours, 7 days, 30 days, never — player chooses). Default: never.
- An optional read-once flag (📐 Design-only). When set, the first reader's act of reading destroys the beacon. Useful for treasure hints and dead-drops. Default: false.
Deployment commits via POST /api/v1/beacons/deploy with {sector_id, message, expiry, read_once}. 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 (📐 Design-only column on Sector).
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). The player can:
- 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. - 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). - Ignore the beacon — it stays in place, visible to other arrivals.
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. - Expiry timer fires — after the configured TTL (24h / 7d / 30d) the beacon 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 if their region has a known dense-traffic context, but the default closes the spam vector.
Schema¶
MessageBeacon¶
Source: services/gameserver/src/models/message_beacon.py (📐 Design-only).
| 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 | When the beacon auto-removes. NULL means "never expires." |
| read_once | Boolean | default false | If true, first read destroys the beacon. 📐 Design-only — initial launch may ship with read_once = false only. |
| 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 (📐 Design-only column) — JSONB array of beacon summaries denormalized for fast sector-view reads. Updated on deploy / salvage / expiry. 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 sanitization (DOMPurify-equivalent at the deploy endpoint).
- 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 (📐 Design-only) — 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. - A per-player trust score read (per the ARIA trust model) — players with very low trust have their beacons auto-flagged for moderator review before becoming player-visible.
Reports against beacons (the player-facing "report" UI on a beacon read view) flag the beacon for moderation. A flagged beacon is hidden from view pending moderator action; the deployer can be warned, time-banned, or have their trust score docked depending on the violation.
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 (📐 Design-only direct messaging) 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¶
- 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 the list — sender, deploy time, preview.
- Click a beacon to read the full message. Costs 0 turns.
- Salvage button on each beacon — costs 1 turn, refunds 250 credits.
- Deploy beacon button in the sector toolbar — costs 5 turns + 500 credits + 1 equipment.
- My Beacons screen shows all beacons the player has deployed across the universe, with read counts and salvage / expire links.
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 + report flow at read | Auto-reject deploy; flag-and-hide on report |
| 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 |
Status¶
📐 Design-only. No
MessageBeacontable, no deploy endpoint, noSector.message_beaconscolumn, no read-once flag exists today. The feature is fully spec'd here and ready for implementation. The anti-griefing layer reuses the existing ARIA content-policy filters; the realtime broadcast reuses the existing bus rooms.
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.