Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ full engineering record.

## [Unreleased]

- Built the review-only Realm Chat V1 foundation as an isolated SpacetimeDB
protocol: server-authored identity/order/time, a private archive, bounded live
projection and history, exactly-once sends, rolling abuse limits, private
context-preserving reports, audited tombstones, and responsive desktop/Mini
App chat surfaces. The feature remains unseeded, unpublishable, and hidden
behind independent server and client gates pending legal and activation review.
- Drafted the next Alpha Terms, Hegemony Social Contract, and Privacy Notice
for a future persistent Realm Chat, including explicit conduct, reporting,
moderation, history, and privacy boundaries. Chat remains disabled pending
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ contributors. This page routes deeper work without duplicating it.
- [Product direction](design/warpkeep-direction.md) — the game's premise and
design principles
- [Roadmap](design/roadmap.md) — what is live, under development, and later
- [Realm Chat V1 implementation](design/realm-chat-v1-implementation.md) —
review-only research, SpacetimeDB authority, abuse controls, and rollout gates
- [Technical architecture](technical-architecture.md) — browser, identity
bridge, SpacetimeDB, rendering, and delivery
- [Lowlands renderer](design/hegemony-lowlands-terrain.md) — terrain,
Expand Down
168 changes: 168 additions & 0 deletions docs/design/realm-chat-v1-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Realm Chat V1 implementation

Status: **review-only; client, server activation, and production publication disabled**

Policy version: `2026-08-03-realm-chat-policy-v1`

Realm channel: `realm:genesis-001`

This document records the product research, technical design, security model,
and release boundary for Warpkeep's first persistent in-game chat. The legal
and product contract remains the controlling source for whether the feature may
ever be activated: [Realm Chat V1 contract](realm-chat-v1-contract.md).

## Research translated into Warpkeep

The implementation follows recurring patterns from established live games,
while avoiding features that would create false expectations in an early Alpha.

| Established pattern | Warpkeep V1 decision |
| --- | --- |
| Final Fantasy XIV lets players create chat tabs and choose which message categories appear in each tab. | V1 has one clearly named Realm channel, but its channel key, status row, and isolated subscription leave room for later user-defined views without changing message authority. |
| Fortnite distinguishes game/party text chat, exposes privacy settings, and places reporting close to the relevant conversation. | The Realm dock makes audience scope explicit, keeps mute local to the browser session, and attaches reporting to one exact message rather than to an unstructured player form. |
| Minecraft's reporting flow includes surrounding chat context and allows players to preview what will be submitted. | Warpkeep records a bounded context range at report time, discloses that behavior before submission, prevents later messages from entering the report, and keeps the evidence private for authorized review. |
| Modern game chat preserves play space: a bounded desktop surface and a dedicated compact/mobile destination are more usable than a full-screen overlay everywhere. | Desktop uses a lower-left dock; compact web and Farcaster Mini Apps use the Realm's existing full-screen destination and single Back-navigation owner. |

Primary product references:

- [Final Fantasy XIV: creating a chat log tab](https://na.finalfantasyxiv.com/uiguide/communication/communication-chat/chat_owntab.html)
- [Fortnite: managing text chat options](https://www.epicgames.com/help/c-1/a202300000011592?lang=en-US)
- [Fortnite: reporting bad player behavior](https://www.epicgames.com/help/c-5719350646299/a202300000017678?lang=en-US)
- [Minecraft: addressing player chat reporting](https://www.minecraft.net/en-us/article/addressing-player-chat-reporting-tool)
- [Minecraft Java 1.19.1 report-context notes](https://feedback.minecraft.net/hc/en-us/articles/34593554333197-Minecraft-Java-Edition-1-19-1)
- [Minecraft accessibility settings](https://help.minecraft.net/hc/en-us/articles/43045760611469)

## Authority model

The browser expresses intent. SpacetimeDB decides identity, admission,
agreement eligibility, channel, message ID, order, time, visibility, rate
limits, report linkage, and moderation state.

```text
admitted player
-> send/report reducer (private intent, caller-derived FID)
-> private archive / receipt / rate / report tables
-> bounded public status + newest 128-message projection
-> isolated browser subscription
-> desktop dock or compact Realm destination
```

The implementation uses the official SpacetimeDB model deliberately:

- reducers are the only message/report mutation boundary;
- public tables contain only channel status and the bounded recent projection;
- private tables contain the permanent archive, channel sequence, rate events,
idempotency receipts, and reports;
- older history is a caller-gated procedure with an exclusive indexed cursor,
at most 50 sequence lookups, and no full-table scan;
- moderator procedures are admin-only, bounded, and read-only unless the named
reducer records an audited state change; and
- generated browser bindings expose only two public tables, two self-service
reducers, and one caller-safe history procedure.

Relevant platform references:

- [SpacetimeDB TypeScript client and subscriptions](https://spacetimedb.com/docs/clients/typescript/)
- [SpacetimeDB table model](https://spacetimedb.com/docs/tables/)
- [SpacetimeDB table access permissions](https://spacetimedb.com/docs/tables/access-permissions/)
- [SpacetimeDB reducers](https://spacetimedb.com/docs/functions/reducers/)
- [SpacetimeDB views and caller-scoped reads](https://spacetimedb.com/docs/functions/views/)

Chat is not joined to Warpkeep's large Realm snapshot. Its two-table
subscription has its own observer and failure boundary, so chat reconnects or
malformed chat rows cannot invalidate terrain, keeps, resources, or Workers.

## Persistence and migration

Protocol V15 appends exactly seven tables after the frozen V14 schema:

| Table | Visibility | Purpose |
| --- | --- | --- |
| `realm_chat_status_v1` | Public | Policy, mode, and projection limits |
| `realm_chat_channel_v1` | Private | Canonical sequence and channel state |
| `realm_chat_message_v1` | Private | Authoritative message archive and moderation evidence |
| `realm_chat_recent_v1` | Public | Exact newest window, capped at 128 rows |
| `realm_chat_rate_event_v1` | Private | Bounded rolling anti-spam evidence |
| `realm_chat_send_receipt_v1` | Private | Exactly-once retry receipts |
| `realm_chat_report_v1` | Private | One caller/message report and frozen context range |

The V14 fixture stays frozen. A separate V15 fixture, static schema checks, and
a disposable populated-database proof establish additive preservation,
idempotent republish, all-seven-table row retention, and refusal of destructive
V15-to-V14 rollback. The canonical production publisher intentionally has no
V15 publication lane in this branch.

## Message and abuse policy

The server normalizes CRLF and Unicode NFC before validation. Candidate V1
limits are 500 Unicode scalars, 2,048 UTF-8 bytes, and eight lines. Controls
that can forge or visually reorder moderation evidence are rejected while
ordinary right-to-left language remains supported.

Accepted messages are limited to one every two seconds, ten in a rolling
minute, sixty in a rolling hour, and no duplicate normalized body from the same
sender within sixty seconds. Rejected attempts consume no quota. State is
bounded per FID and corrupt or oversized ledgers fail closed.

Each send uses a canonical UUID request key. A client retries an ambiguous
timeout with the same key and body for a bounded window; the server either
returns the original success or rejects a conflicting replay. Message IDs are
server-generated UUIDv7 values and sequence/time are server-authored.

Reports are message-local, caller-bound, private, and idempotent. Self-reporting
and duplicate report mutation are rejected. The recorded context ends at the
last sequence that existed when the report was created, so later conversation
cannot silently alter evidence. Public moderation replaces the body with a
tombstone while the private admin evidence procedure retains the original text.
Reporting never automatically hides a message or punishes a player.

## Player experience

- The launcher shows unread count without stealing focus.
- Opening at the bottom marks the current live window read. New messages do not
force-scroll a player who is reading earlier history.
- The composer preserves a failed draft, sends on Enter, inserts a line break
with Shift+Enter, and does not send during IME composition.
- Sender portraits open a small keeper card with keep location, session mute,
exact-message report, and safe plain-text copy controls.
- Messages are rendered as text only. V1 has no HTML, automatic links, embeds,
attachments, or rich previews.
- Compact chat participates in the Realm's single Farcaster Back boundary:
Back closes a report first, then chat, then resumes normal Realm navigation.
- The message log, status announcements, report dialog, focus containment,
expanded relationships, reduced motion, safe areas, and focus restoration
are keyboard and assistive-technology aware.

## Deliberate V1 non-goals

V1 does not claim typing indicators, online presence, delivery/read receipts,
direct messages, guild chat, proximity chat, voice, translation, reactions,
editing, deletion by players, link previews, attachments, or push
notifications. In particular, it does not infer presence from a socket or
invent typing state that SpacetimeDB does not authoritatively persist.

These can be evaluated later as separate privacy, retention, moderation, and
authority changes. Channel extensibility is preserved without exposing those
features prematurely.

## Activation checklist

Merging this implementation must not activate or publish chat. Activation
requires a separate reviewed change that records all of the following:

1. owner and qualified legal approval, including the unresolved age/minor
policy and an explicit retention/erasure schedule;
2. exact approved legal, policy, client, server, schema, and generated-binding
versions;
3. a V15 production predecessor, additive migration receipt, protected admin
inspection, and exact post-publication checkpoint;
4. staged channel health, bounded projection/archive parity, moderator access,
kill-switch, and canary evidence;
5. desktop, compact web, and Farcaster Mini App accessibility/abuse QA;
6. named rollback owner and evidence-preserving incident procedure; and
7. a separate commit changing both the server activation compile gate and the
client entry gate only after the active channel is verified.

Until then, the client flag is `false`, server activation is not compiled, the
channel is unseeded, the production publisher rejects V15 mutation, and no chat
data is collected.
15 changes: 15 additions & 0 deletions docs/security/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ operation. Anonymous visitors do not connect to the game database.
| World and castle state | Transactional integrity and server-enforced ownership. |
| Deployment authority | Least privilege, reviewed changes, protected branches, and reproducible artifacts. |
| Player privacy | Minimum collection, bounded presentation fields, redacted diagnostics, and private operational records. |
| Realm Chat and moderation evidence | Server-authored identity/order/time, bounded public history, private archive/reports/rate state, audited moderation, and no activation before approved retention and minor-participation policy. |

## Trust boundaries

Expand Down Expand Up @@ -143,6 +144,10 @@ operation. Anonymous visitors do not connect to the game database.
- Public Farcaster presentation is sanitized and optional. A tab cache may hold
only public display fields, may merge only after a successful same-FID
refresh, and never restores authority.
- Review-only Realm Chat keeps its permanent archive, rate events, idempotency
receipts, reports, and moderator evidence private. Only one status row and an
exact newest 128-message projection can become public after separate
activation. The ordinary Realm snapshot never absorbs chat history.

### Input, transport, and abuse controls

Expand All @@ -162,6 +167,10 @@ operation. Anonymous visitors do not connect to the game database.
re-enter the same reviewed-host, byte, decode, and static-format checks.
- Public error messages and logs omit proof material, tokens, cookies, QR
payloads, relay secrets, identities, private rows, and credentialed URLs.
- Realm Chat normalizes Unicode and line endings, rejects evidence-spoofing
controls, applies exact rolling per-FID limits, and records reports against a
frozen context range. Report submission does not automatically hide content
or punish another player.

### Operations and software supply chain

Expand Down Expand Up @@ -199,6 +208,8 @@ operation. Anonymous visitors do not connect to the game database.
| Dependency or CI compromise | Lockfiles, audits, checksum and action pinning, job separation, and branch protection reduce exposure; provider compromise remains possible. |
| Operator compromise | Destination allowlists, short-lived tokens, private secret storage, and approvals reduce blast radius; a compromised operator account or workstation remains a critical incident. |
| Misleading Marks or Alpha expectations | Product copy states that Marks are non-transferable and have no cash value or guaranteed reward; formal legal and privacy review remains necessary as use expands. |
| Chat spam, harassment, or forged moderation context | Server-derived identity/order/time, bounded rate ledgers, exact-message reporting, frozen surrounding context, local mute, private review, and audited tombstones reduce abuse; human moderation quality and response time remain operational risks. |
| Chat archive or report exposure | Private tables, narrow generated player bindings, bounded procedures, body-free public tombstones, and isolated subscriptions reduce exposure; operator compromise and approved retention/erasure handling remain residual risks. |

## Residual risks and current limitations

Expand All @@ -212,6 +223,10 @@ operation. Anonymous visitors do not connect to the game database.
- Public Realm projections are observable to admitted clients by design.
Privacy classification and retention must be revisited before adding new
player-linked data.
- Realm Chat is disabled and unpublishable in the review branch. Activation is
blocked until qualified review approves retention/erasure, data-subject
handling, age/minor participation, moderation access, incident response, and
exact production migration checkpoints.
- Hosting-layer security headers, including HSTS, require ongoing deployment
verification. The production CSP keeps exact source and egress allowlists,
but SpacetimeDB 2.6.1 requires a narrowly scoped `script-src 'unsafe-eval'`
Expand Down
7 changes: 7 additions & 0 deletions docs/technical-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ browser cannot choose an FID, castle owner, balance, timer, or outcome through
request fields. Schema changes are additive because deployed tables and
generated client bindings must remain compatible.

A review-only Realm Chat V1 implementation is isolated from the large Realm
snapshot. SpacetimeDB owns sender identity, order, time, anti-abuse state,
history, reporting, and moderation evidence; the browser may subscribe only to
one status row and a bounded recent projection. Independent client and server
gates plus a blocked production publication lane keep it unavailable pending a
separate legal and operational activation review.

The module guide, local commands, and schema notes live in
[`spacetimedb/README.md`](../spacetimedb/README.md).

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"stdb:generate": "node scripts/generate-spacetime-bindings.mjs",
"stdb:verify-bindings": "node scripts/verify-spacetime-bindings.mjs",
"stdb:build-v14-migration-fixture": "spacetime build --module-path spacetimedb/migration-fixtures/additive-v14-schema",
"stdb:build-v15-migration-fixture": "spacetime build --module-path spacetimedb/migration-fixtures/additive-v15-schema",
"stdb:verify-additive-migration": "node scripts/verify-spacetime-additive-migration.mjs",
"stdb:verify-worker-migration": "node scripts/verify-castle-worker-additive-migration.mjs",
"stdb:verify-access-request-migration": "node scripts/verify-access-request-additive-migration.mjs",
Expand Down
17 changes: 15 additions & 2 deletions scripts/publish-spacetime-dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2682,12 +2682,13 @@ function validateMigrationArtifactReceiptShape(receipt) {
receipt === null
|| typeof receipt !== 'object'
|| Object.keys(receipt).sort().join(',')
!== 'artifactDigest,artifactPath,v11TableSchemaDigest,v12TableSchemaDigest,v13TableSchemaDigest,v14TableSchemaDigest'
!== 'artifactDigest,artifactPath,v11TableSchemaDigest,v12TableSchemaDigest,v13TableSchemaDigest,v14TableSchemaDigest,v15TableSchemaDigest'
|| receipt.artifactPath !== PROVEN_ARTIFACT_PATH
|| !SHA256_DIGEST.test(receipt.v11TableSchemaDigest ?? '')
|| !SHA256_DIGEST.test(receipt.v12TableSchemaDigest ?? '')
|| !SHA256_DIGEST.test(receipt.v13TableSchemaDigest ?? '')
|| !SHA256_DIGEST.test(receipt.v14TableSchemaDigest ?? '')
|| !SHA256_DIGEST.test(receipt.v15TableSchemaDigest ?? '')
|| !SHA256_DIGEST.test(receipt.artifactDigest ?? '')
) {
fail('The additive migration proof artifact receipt was invalid.');
Expand All @@ -2698,6 +2699,7 @@ function validateMigrationArtifactReceiptShape(receipt) {
v12TableSchemaDigest: receipt.v12TableSchemaDigest,
v13TableSchemaDigest: receipt.v13TableSchemaDigest,
v14TableSchemaDigest: receipt.v14TableSchemaDigest,
v15TableSchemaDigest: receipt.v15TableSchemaDigest,
artifactDigest: receipt.artifactDigest,
});
}
Expand All @@ -2724,6 +2726,7 @@ export function parseMigrationProofReceipt(output) {
v12TableSchemaDigest: proofReceipt.v12TableSchemaDigest,
v13TableSchemaDigest: proofReceipt.v13TableSchemaDigest,
v14TableSchemaDigest: proofReceipt.v14TableSchemaDigest,
v15TableSchemaDigest: proofReceipt.v15TableSchemaDigest,
artifactDigest: proofReceipt.artifactDigest,
});
}
Expand Down Expand Up @@ -3840,7 +3843,7 @@ export async function publishModule(
if (targetDatabase !== CANONICAL_DATABASE_IDENTITY) {
fail('The production publish target was not the pinned canonical database identity.');
}
const artifact = validateMigrationArtifactReceiptShape(artifactReceipt);
const artifact = verifyMigrationArtifactReceipt(artifactReceipt);
const artifactSnapshot = createPrivatePublishSnapshot(
artifact.artifactPath,
artifact.artifactDigest,
Expand Down Expand Up @@ -3929,6 +3932,15 @@ export async function publishModule(
}
}

/**
* Keep the additive v15 artifact testable without granting it a production
* publication lane. Activation requires a later evidence-backed change with
* exact predecessor and post-publication checkpoints.
*/
export function requireRealmChatV15ProductionPublishReady() {
fail('Realm Chat protocol v15 is review-only and cannot be published by this build.');
}

async function main() {
const {
dryRun,
Expand Down Expand Up @@ -3971,6 +3983,7 @@ async function main() {
console.log(`Dry run: verified the pinned CLI, current additive migration, founded-state expectation contract, explicit ${resourceRolloutStage} resource stage, explicit ${genesisWorldRolloutStage} Genesis world stage, explicit ${workerRolloutStage} Worker stage, explicit ${workerModulePredecessor} module predecessor, explicit ${workerForwardRepair} Worker forward-repair selection, and ${issuer}; would update the canonical existing database without deleting data.`);
return;
}
requireRealmChatV15ProductionPublishReady();
await validateIssuerDeployment(issuer);
attestCanonicalDatabase(executable);
if (
Expand Down
Loading
Loading