Skip to content
Open
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
99 changes: 98 additions & 1 deletion docs/about-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,22 @@ pnpm stdb:start
pnpm stdb:publish:clear
```

Generate frontend bindings after schema/reducer changes:
Generate frontend bindings after schema/reducer changes.

The root script regenerates only `apps/tma/src/module_bindings`:

```bash
pnpm stdb:generate
```

The payments bindings are not covered by that script and must be regenerated separately:

```bash
spacetime generate --lang typescript --out-dir apps/payments/src/module_bindings --module-path apps/spacetime/spacetimedb
```

Both commands must be run after every SpacetimeDB schema change.

Run frontend locally against cloud:

```bash
Expand Down Expand Up @@ -263,3 +273,90 @@ pnpm test:stdb-local-scenarios:full
- The frontend provider contract still includes `submitMove()` as the UI-facing action, but the SpacetimeDB provider implements it by committing and revealing under the hood.
- `packages/shared/src/constants.ts` defines `MAX_ROUNDS = 9`, while `apps/spacetime/spacetimedb/src/index.ts` defines `MAX_ROUNDS = 5`. This may be intentional or may cause behavior differences between mock/shared logic and real backend.
- The backend duplicates parts of game logic instead of importing `packages/shared`, so matrix and energy changes must be kept synchronized.

## Architecture Analysis

### Dependency Graph

```
@elmental/shared
└── consumed by: apps/tma, apps/server
└── not consumed by: apps/payments (independent domain, no shared dep)

apps/tma
└── depends on: @elmental/shared, spacetimedb SDK, socket.io-client, zustand, telegram-apps/sdk

apps/server (legacy)
└── depends on: @elmental/shared, express, socket.io, ioredis, pg

apps/payments
└── depends on: spacetimedb SDK, undici
└── no dependency on apps/tma, apps/server, or @elmental/shared

apps/spacetime/spacetimedb
└── single-file module, cannot import workspace packages
└── generates bindings consumed by: apps/tma, apps/payments
```

No app imports from another app. No circular dependencies exist.

### Layer Separation

The monorepo has four distinct runtime layers:

| Layer | App | Responsibility |
|-------|-----|----------------|
| Frontend | `apps/tma` | React UI, Telegram SDK, provider contract |
| Authoritative backend | `apps/spacetime/spacetimedb` | Game state, reducers, matchmaking, settlement — this is the server |
| Payments | `apps/payments` | Stars invoicing, refunds, wallet history, admin |

`apps/server` is a deprecated Express/Socket.io experiment that predates the SpacetimeDB architecture. It is not part of any active flow. TMA has no references to it; it is absent from `docker-compose.selfhost.yml`. Do not route gameplay through it. All server-side game logic — matchmaking, move resolution, balance mutations, timeouts — lives in SpacetimeDB reducers. If a new server-side component is needed, extend `apps/spacetime/spacetimedb` or create a dedicated new app.

### What Is Properly Shared

`packages/shared` is the correct place for the shared frontend/test game rules layer. It contains:

- `types.ts`: canonical enums and interfaces (`MoveId`, `GameMode`, `RoundResult`, `MatchState`)
- `constants.ts`: energy values, move costs, regen tables, ELO parameters, economy constants
- `game-logic.ts`: `resolveRound`, `calculateElo`, `calculateEnergy`, `resolveOverclock`

Frontend UI uses `@elmental/shared` for rendering decisions (move costs, labels). Production gameplay is server-authoritative: SpacetimeDB is the source of truth for real matches. The SpacetimeDB module duplicates equivalent game rules because it cannot import workspace packages — this is an architectural constraint, not a design choice. Backend rules and `packages/shared` must be kept in sync manually.

### Known Duplications

**1. Game constants and logic — `packages/shared` vs `apps/spacetime/spacetimedb/src/index.ts`**

SpacetimeDB modules compile to a single self-contained file and cannot consume npm workspace packages. As a result, move costs, energy constants, regen values, and the outcome matrix are defined in both places. Any change to game rules requires a matching update in both files. `scripts/check-matrix-parity.mjs` exists to catch divergence in the move matrix, but does not cover all constants. The `MAX_ROUNDS` discrepancy (9 in shared, 5 in spacetime) is an example of what can silently drift.

**2. Telegram `initData` validation — `apps/server/src/auth/index.ts` and `apps/payments/src/telegramInitData.ts`**

Both implement HMAC-SHA256 validation of Telegram `initData` for the same purpose, but with different implementations. `apps/payments/src/telegramInitData.ts` is the current reference: it uses a timing-safe hex comparison (`timingSafeHexEqual`) and requires a valid `auth_date` (missing or expired → reject). The legacy `apps/server` version uses a direct string comparison (`===`) and treats `auth_date` as optional — only checked if present. Because `apps/payments` does not depend on `apps/server` (and should not), this duplication is currently necessary. If `@elmental/shared` ever gains a browser/Node-compatible crypto utility layer with no external dependencies, this validation could move there, using the payments implementation as the basis.

**3. SpacetimeDB `module_bindings` — `apps/tma/src/module_bindings` and `apps/payments/src/module_bindings`**

These are auto-generated from the same SpacetimeDB schema and are expected to be identical at any given schema version. They are not manually authored. Do not consolidate them into a shared package — they must stay co-located with each consumer so that `pnpm stdb:generate` can target each app independently.

### Layer Violation Checks

No violations were found at the time of this analysis:

- `apps/tma` does not import from `apps/server` or `apps/payments`.
- `apps/payments` does not import from `apps/tma` or `apps/server`.
- `apps/server` does not import from `apps/tma` or `apps/payments`.
- All three apps consume `@elmental/shared` only through the workspace package name, not via relative paths.
- Generated `module_bindings` are not imported outside their owning app. `spacetimeProvider.ts` is the only non-generated file in `apps/tma` that may import from `src/module_bindings`.

### Synchronization Risks

The following pairs must stay in sync manually and are not enforced by the type system:

| What | Location A | Location B | Guard |
|------|-----------|-----------|-------|
| Move outcome matrix | `packages/shared/src/game-logic.ts` | `apps/spacetime/spacetimedb/src/index.ts` | `pnpm test:matrix-parity` |
| Energy and move cost constants | `packages/shared/src/constants.ts` | `apps/spacetime/spacetimedb/src/index.ts` | None — manual |
| `MAX_ROUNDS` | `packages/shared/src/constants.ts` (= 9) | `apps/spacetime/spacetimedb/src/index.ts` (= 5) | None — diverged |
| SpacetimeDB schema | `apps/spacetime/spacetimedb/src/index.ts` | `apps/tma/src/module_bindings`, `apps/payments/src/module_bindings` | Run both generate commands from **Common Commands** — `pnpm stdb:generate` only covers `apps/tma`; payments must be regenerated separately |

When changing game rules, update both `packages/shared` and `apps/spacetime/spacetimedb/src/index.ts`, then run the full verification sequence described in **Common Commands**.

**Schema/bindings rule:** every SpacetimeDB schema change must regenerate bindings for every active consumer (`apps/tma` and `apps/payments`). If a consumer intentionally receives no updated bindings — for example, because a new table or reducer is not relevant to it — that decision must be recorded explicitly in the PR description.