This document defines:
The design is intentionally offline-first during active play. Once a game has started and the mobile application has downloaded its signed game package, routine gameplay must not require a live server connection.
The backend is the authoritative source for:
The mobile app is responsible for:
The backend is not involved in routine:
A local-only game cannot be fully cheat-proof on a modified or rooted device. The design below protects against casual spoofing, preserves privacy, and provides signed evidence for later review, but it cannot make a compromised client fully trustworthy without moving adjudication back to the server.
The application should therefore be described as:
Offline-capable and locally enforced, with server-authorized setup, moderation, and post-game audit support.
| Term | Definition |
|---|---|
user_id |
Persistent account identifier used between games. |
game_id |
Identifier for one game session. |
game_member_id |
Pseudonymous player identifier unique to one game. |
ephemeral_peer_id |
Short-lived Bluetooth identifier that rotates during a game. |
| Green Light | Signed location or venue authorization permitting a game to be created. |
| Game Bootstrap Bundle | Signed package downloaded by each player at game start. |
| Role Certificate | Server-signed proof binding a game member to a role. |
| Boot Directive | Server-signed instruction removing a player from active participation. |
| Local Event Ledger | Device-side append-only record of location checks, claims, guesses, receipts, and round events. |
The app communicates with the server only when it needs to establish identity, create or join a game, download an authoritative game package, report telemetry, receive a moderation directive, or synchronize final results.
All endpoints use JSON over TLS.
| Requirement | Standard |
|---|---|
| API prefix | /v1 |
| Authentication | Bearer access token |
| Mutation retries | Idempotency-Key request header |
| Timestamps | ISO 8601 UTC |
| Identifiers | UUID or opaque server-generated IDs |
| Transport security | HTTPS only |
| Error format | Structured JSON error object |
| Gameplay package integrity | Server signature verified by app |
| Sensitive local storage | OS keychain / keystore plus encrypted local database |
{
"error": {
"code": "GREEN_LIGHT_EXPIRED",
"message": "This location authorization is no longer valid.",
"request_id": "uuid"
}
}
Common status codes:
| Code | Meaning |
|---|---|
400 |
Invalid request format |
401 |
Missing or expired authentication |
403 |
Authenticated but unauthorized |
404 |
Game, location, or resource not found |
409 |
Invalid game state or duplicate action |
410 |
Expired green light or expired package |
422 |
Valid request format but invalid game rule |
429 |
Rate limited |
503 |
Service temporarily unavailable |
Authentication exists only to establish a persistent player identity, preserve game history, manage verified appearance traits, and enforce game-related permissions.
It is not intended to create a social network, friend graph, or public player directory.
| Method and Route | Mobile Use | Server Response | Local Effect |
|---|---|---|---|
POST /v1/auth/session |
Log in | Access token, refresh token, persistent user ID | Creates secure local session |
POST /v1/auth/session/refresh |
Renew expired session | New access token | Preserves session without re-login |
DELETE /v1/auth/session |
Log out | Confirmation | Removes local credentials |
GET /v1/me/game-identity |
Restore session or prepare for game | User ID, game profile status, role capabilities | Determines eligibility to join or create |
PUT /v1/me/game-profile |
Save gameplay traits | Updated profile status | Stores verified appearance traits |
POST /v1/me/game-profile/photo-verification |
Submit confirmation photo | Verification pending or approved status | Enables identity readiness for games |
PUT /v1/me/game-device |
Register device capabilities and optional push token | Device registration status | Supports compatibility checks and boot notices |
{
"appearance_traits": {
"gender": "female",
"hair_color": "brown",
"skin_color": "medium"
},
"photo_verification_status": "approved",
"game_identity_status": "eligible"
}
The server should store only gameplay-required profile details. The mobile app should not expose a player’s real name, email address, persistent user ID, or uploaded verification photo to nearby players.
A player may create a game only after receiving a valid venue or location green light.
A green light may be delivered through a QR code, NFC tag, venue-admin app, event coordinator token, or a similar controlled mechanism.
| Method and Route | Mobile Use | Required Role | Result |
|---|---|---|---|
POST /v1/green-lights/validate |
Validate scanned or entered green light | Authenticated player | Returns a creation grant or rejection |
GET /v1/green-lights/{green_light_id} |
Retrieve limited authorization metadata | Authenticated player | Confirms venue, expiration, and allowed game template |
{
"green_light_token": "signed-token-from-location",
"device_time": "2026-06-20T18:45:00Z",
"requested_game_template": "standard_social_deduction"
}
{
"creation_grant": "opaque-one-time-grant",
"venue_id": "uuid",
"venue_name": "Example Venue",
"game_zone_id": "uuid",
"expires_at": "2026-06-20T20:00:00Z",
"permitted_templates": [
"standard_social_deduction"
],
"max_players": 40,
"max_rounds": 8
}
The app must not treat a locally scanned QR code as sufficient authorization. It must be validated by the server before a game can be created.
| Method and Route | Mobile Use | Required Role | Result |
|---|---|---|---|
GET /v1/games?state=joinable |
Find public, joinable games | Authenticated player | Joinable game summaries |
POST /v1/games |
Create game from valid green light | Eligible player | Draft game and join code |
GET /v1/games/{game_id} |
View lobby or game summary | Member, creator, moderator | Current authorized game summary |
PATCH /v1/games/{game_id} |
Update allowed game settings | Creator, moderator | Updated configuration |
POST /v1/games/{game_id}/members |
Join game with join code | Eligible player | Membership confirmation |
DELETE /v1/games/{game_id}/members/me |
Leave before game begins | Game member | Membership removed |
POST /v1/games/{game_id}/start |
Start game and lock membership | Creator, moderator | Game transitions to active state |
{
"creation_grant": "opaque-one-time-grant",
"name": "Saturday Night Squatchin'",
"visibility": "private",
"max_players": 24,
"round_count": 5,
"start_mode": "creator_start",
"game_template": "standard_social_deduction"
}
{
"game_id": "uuid",
"game_state": "lobby",
"join_code": "MOSS-ELK",
"join_code_expires_at": "2026-06-20T19:15:00Z",
"creator_game_member_id": "uuid"
}
| Parameter | Player May Configure | Server Constraint |
|---|---|---|
| Public or private visibility | Yes | Venue policy may override |
| Game name | Yes | Content filtering applies |
| Maximum players | Yes, within limits | Cannot exceed green-light limit |
| Round count | Yes, within limits | Cannot exceed venue template |
| Start mode | Yes | Must use approved options |
| Join-code sharing | Yes | Code expiration is server-controlled |
| Accessibility preference | Yes | Must remain compatible with venue routes |
| Parameter | Reason |
|---|---|
| Role ratio | Prevents creator manipulation |
| Approved locations | Must stay inside venue game zone |
| Geofence radius | Prevents unsafe or unusable route design |
| Location safety restrictions | Controlled by venue policy |
| Allowed round count range | Controlled by green light |
| Role rules | Prevents ad hoc rule tampering |
| Package signature | Guarantees app integrity checks |
| Moderator authority | Controlled by RBAC |
| Method and Route | Mobile Use | Routine Frequency | Result |
|---|---|---|---|
GET /v1/games/{game_id}/bootstrap |
Download signed game package after start | Once; recovery only afterward | Role assignment, rules, route package, trust anchors |
POST /v1/games/{game_id}/sync |
Reconcile after reconnect or app restart | Recovery only | Package revisions, signed directives, server state |
GET /v1/games/{game_id}/membership-status |
Check status after a boot notice | Only when booted or uncertain | Active, spectator, removed, or terminated |
POST /v1/games/{game_id}/boot-acknowledgements |
Confirm client received boot directive | Once per directive | Audit confirmation |
POST /v1/games/{game_id}/results |
Upload final game summary | At end or reconnect | Result accepted or queued for review |
GET /v1/games/{game_id}/summary |
Load final game summary | At game completion | Final outcomes and personal stats |
The bootstrap bundle is the app’s offline authority package. It must be signed by the backend and validated before the game can begin.
{
"game_id": "uuid",
"package_version": 1,
"server_time": "2026-06-20T19:00:00Z",
"game_rules": {
"sasquatch_must_change_locations": true,
"hunter_may_repeat_locations": true,
"sasquatch_appearance_claims_must_match_profile": true,
"hunter_location_claims_must_match_validated_location": true
},
"member_identity": {
"game_member_id": "uuid",
"initial_ephemeral_peer_id": "rotating-token"
},
"private_assignment": {
"role": "sasquatch",
"role_certificate": "server-signed-certificate"
},
"round_route_packages": [
{
"round_id": 1,
"location_node_id": "uuid",
"latitude": 37.2701,
"longitude": -76.7075,
"radius_meters": 35,
"entry_challenge": {
"type": "question",
"prompt": "What color is the sign near this location?",
"answer_verifier": "salted-hash"
}
}
],
"trust_anchors": {
"server_public_signing_key": "public-key-material"
},
"signature": "server-signature"
}
The app should encrypt the stored bootstrap bundle using device-backed secure storage where available.
Telemetry supports operational reliability, event safety, compatibility analysis, and delivery of server directives.
Telemetry must not be used as a routine substitute for local GPS validation or Bluetooth gameplay.
| Method and Route | Purpose | Server Response |
|---|---|---|
POST /v1/games/{game_id}/telemetry/batches |
Submit queued operational telemetry | Receipt, optional directives, retry guidance |
POST /v1/client-errors/batches |
Report non-game application failures | Receipt only |
{
"installation_id": "uuid",
"events": [
{
"event_id": "uuid",
"occurred_at": "2026-06-20T19:22:10Z",
"round_id": 3,
"event_type": "heartbeat",
"battery_percent": 72,
"network_state": "cellular",
"bluetooth_state": "enabled",
"gps_state": "location_valid",
"gps_accuracy_bucket": "0_to_25_meters",
"app_version": "1.0.0"
}
]
}
By default, telemetry should report a location-validation result, not raw GPS coordinates or a continuous route history.
{
"accepted_event_ids": [
"uuid"
],
"retry_after_seconds": 300,
"directives": [
{
"directive_id": "uuid",
"type": "boot_player",
"effective_at": "2026-06-20T19:24:00Z",
"reason_code": "moderator_removal",
"signature": "server-signature"
}
]
}
A boot directive must be server-signed. The app must never remove a player based solely on a Bluetooth message from another device.
These endpoints are available only where a player has a game-relevant creator, moderator, or administrator role.
| Method and Route | Permitted Role | Purpose |
|---|---|---|
POST /v1/games/{game_id}/moderation/remove-members |
Moderator, administrator | Remove one or more players |
POST /v1/games/{game_id}/moderation/pause |
Moderator, administrator | Pause active game |
POST /v1/games/{game_id}/moderation/resume |
Moderator, administrator | Resume paused game |
POST /v1/games/{game_id}/moderation/terminate |
Moderator, administrator | End game immediately |
GET /v1/games/{game_id}/moderation/audit-summary |
Moderator, administrator | Review server-side game events |
No general-purpose administrative portal, generic role management system, or unrelated account settings workflow is in scope for this mobile design.
The application must maintain a local encrypted game store so that active gameplay can continue during intermittent connectivity.
| Local Object | Purpose |
|---|---|
| Session state | Current authenticated session and refresh eligibility |
| Game bootstrap bundle | Signed game rules, assignments, and route data |
| Current round state | Current round, status, timer, entry completion |
| Location history | Previously validated location-node IDs |
| GPS validation receipts | Local evidence that player entered a valid geofence |
| Nearby peer cache | Temporary nearby-player identifiers and handshake state |
| Claim ledger | Claims sent and received during the active game |
| Guess ledger | Local guesses, outcomes, and proof receipts |
| Moderation directive cache | Signed pause, boot, resume, or termination directives |
| Telemetry queue | Unsent operational events |
| Result synchronization queue | Final result payload awaiting delivery |
The application should retain only the current game’s detailed ledger by default. Historical detail should be minimized or deleted after the retention period defined by product policy.
The app validates GPS locally against the signed location package downloaded at game start.
The app must not contact the server merely to decide whether the player is at the correct location.
| Input | Source |
|---|---|
| Current latitude and longitude | Device location services |
| Horizontal accuracy | Device location services |
| Timestamp | Device location services |
| Assigned location coordinates | Signed bootstrap package |
| Geofence radius | Signed bootstrap package |
| Required dwell time | Signed game rules |
| Prior round location node | Local history |
A location is considered valid only when all required conditions are met:
For example:
location_node_id in consecutive rounds.Consumer-device GPS is not fully tamper-proof. The app should detect obvious anomalies, such as impossible jumps, stale readings, disabled providers, or poor accuracy, but the system must not claim that it can perfectly prevent location spoofing without dedicated attestation or server-side tracking.
Each round requires the player to complete a local entry step after passing GPS validation.
Possible entry mechanisms include:
The bootstrap package should contain a verifier rather than a plaintext answer where practical.
{
"event_id": "uuid",
"round_id": 3,
"location_node_id": "uuid",
"validation_type": "question",
"validated_at": "2026-06-20T19:20:00Z",
"result": "passed",
"local_event_hash": "hash"
}
The mobile app must use a transport abstraction so that platform-specific nearby networking can be implemented without changing game logic.
PeerTransport
├── advertisePresence()
├── scanForPeers()
├── establishSecureSession()
├── sendClaim()
├── sendGuess()
├── receiveRoleReceipt()
└── closeSession()
A Bluetooth discovery beacon must contain no real name, email address, persistent user ID, raw GPS coordinate, or appearance trait.
{
"protocol_version": 1,
"game_id_hash": "truncated-game-hash",
"round_id": 3,
"ephemeral_peer_id": "rotating-random-token",
"nonce": "one-time-random-value"
}
Bluetooth signal strength may be used as a rough eligibility filter, but it should not be treated as an exact distance measurement.
The app must present role-specific claim interfaces.
| Claim Type | App Behavior |
|---|---|
| Appearance | Locked to verified gameplay profile; cannot be manually edited |
| Current location | Player may choose or enter a false claim |
| Prior locations | Player may choose or enter a false claim |
| Movement | App requires a different validated location node each round |
| Claim Type | App Behavior |
|---|---|
| Appearance | Player may select a claim that differs from profile |
| Current location | Derived from current locally validated location; not editable |
| Prior locations | Derived from local validated history when required |
| Movement | App permits the same location node in consecutive rounds |
{
"message_type": "claim",
"round_id": 3,
"sender_ephemeral_peer_id": "token",
"appearance_claim": {
"hair_color": "black",
"skin_color": "medium"
},
"location_claim": {
"label": "Location 4"
},
"sent_at": "2026-06-20T19:24:00Z",
"message_signature": "device-session-signature"
}
Claims are local game messages. They are not sent to the server during normal play.
A player must be able to submit a guess only after a valid nearby-device session has been established.
A guess may be submitted only when:
{
"message_type": "guess",
"guess_id": "uuid",
"round_id": 3,
"target_game_member_id": "uuid",
"guessed_role": "sasquatch",
"sent_at": "2026-06-20T19:25:00Z",
"event_chain_hash": "hash"
}
At game bootstrap, each player receives a private, server-signed role certificate.
{
"game_id": "uuid",
"game_member_id": "uuid",
"role": "sasquatch",
"issued_at": "2026-06-20T19:00:00Z",
"certificate_id": "uuid",
"signature": "server-signature"
}
The target app discloses its role certificate only after a valid local guess workflow has completed. The guessing app validates the server signature and compares the guessed role with the certified role.
This permits local resolution without contacting the server.
{
"guess_id": "uuid",
"target_game_member_id": "uuid",
"guessed_role": "sasquatch",
"certified_role": "sasquatch",
"outcome": "correct",
"role_certificate": "server-signed-role-certificate",
"receipt_hash": "hash"
}
The target device possesses its own role certificate. A modified client could disclose it early or attempt to bypass local UI restrictions. This design prevents routine spoofing and provides evidence for review, but it does not provide absolute anti-cheat protection against compromised devices.
The app must continue local gameplay without the server after a valid bootstrap package has been downloaded.
When connectivity returns, the app must not overwrite local game state indiscriminately.
The app should:
Telemetry is operational data, not a gameplay adjudication channel.
| Event | Trigger |
|---|---|
| App launch | App initializes |
| Game bootstrap accepted | Game package validated |
| GPS state changed | Enabled, disabled, valid, invalid |
| Bluetooth state changed | Enabled, disabled, unavailable |
| Round entered | Local entry validation succeeds |
| App backgrounded or resumed | Lifecycle transition |
| Significant error | Recoverable or nonrecoverable fault |
| Periodic heartbeat | Configurable interval while active |
| Game completion | Final local state reached |
| Attempt | Retry Timing |
|---|---|
| First retry | Approximately 1 minute with jitter |
| Second retry | Approximately 5 minutes with jitter |
| Third retry | Approximately 15 minutes with jitter |
| Later retries | Exponential backoff with upper limit |
| Maximum retention | Product-configurable; recommended 24 hours |
Telemetry events must contain unique event IDs so duplicates can be safely accepted and ignored by the server.
The mobile application must:
The Squatchin' backend is a control plane and persistence system. It prepares games, assigns roles, authorizes venue use, distributes signed offline packages, receives telemetry, applies moderation decisions, and stores final outcomes.
It is not the real-time engine for every player interaction.
| Service | Responsibilities |
|---|---|
| Authentication and Game Identity Service | Authenticate users and issue game-scoped identity context |
| Gameplay Profile Service | Store verified appearance traits and photo-verification status |
| Green-Light Service | Validate venue authorization and issue game-creation grants |
| Game Orchestration Service | Create games, manage lobbies, assign roles, lock membership, start games |
| Location Package Service | Build and sign role-aware offline location and rule packages |
| Telemetry Service | Accept batched operational events and return directives |
| Moderation Service | Pause, resume, remove, or terminate game participation |
| Results and Audit Service | Persist final results, event digests, anomaly flags, and summaries |
The backend exposes the following public API groups:
/v1/auth
/v1/me
/v1/green-lights
/v1/games
/v1/games/{game_id}/telemetry
/v1/games/{game_id}/moderation
/v1/client-errors
All APIs except session creation and health monitoring require authentication.
A player may create a game only after obtaining a valid green light.
A valid green light must contain or reference:
| Field | Purpose |
|---|---|
green_light_id |
Unique identifier |
venue_id |
Venue or event owner |
game_zone_id |
Authorized map area |
issued_at |
Issue timestamp |
expires_at |
Expiration timestamp |
allowed_templates |
Permitted game modes |
max_players |
Venue capacity restriction |
max_rounds |
Venue rule restriction |
revocation_version |
Revocation validation |
| Signature | Proves venue or backend authorization |
The backend must verify that the green light has not expired, been revoked, been used beyond policy, or been presented for an unauthorized game template.
| Member State | Meaning |
|---|---|
joined |
Joined lobby but game has not started |
active |
Eligible for active gameplay |
spectator |
May observe but cannot make gameplay actions |
boot_pending |
Server directive issued but not yet acknowledged |
booted |
Removed from active gameplay |
left |
Voluntarily left before start |
disconnected |
Informational telemetry state only |
Role assignment occurs once, when the game transitions from starting to active.
The backend must:
assignRoles(game_id):
members = loadEligibleMembers(game_id)
policy = loadRolePolicy(game_id)
assert policy.minimum_players <= members.count
random_seed = secureRandom()
assignment_commitment = sha256(random_seed)
shuffled_members = deterministicShuffle(members, random_seed)
role_map = allocateRoles(shuffled_members, policy)
begin transaction
storeAssignmentCommitment(game_id, assignment_commitment)
for member in role_map:
certificate = signRoleCertificate(
game_id,
member.game_member_id,
member.role
)
storePrivateAssignment(member, certificate)
transitionGame(game_id, "active")
commit transaction
The assignment seed may be revealed after the game ends if the product later wants to offer fairness verification.
The backend must generate all data necessary for normal gameplay before active rounds begin.
The app must not need to request the next location from the server every round.
The package-generation service must:
| Component | Visibility |
|---|---|
| Common game rules | All members |
| Venue safety rules | All members |
| Game-scoped public key | All members |
| Player game member ID | Individual member |
| Player role | Individual member |
| Role certificate | Individual member |
| Player route package | Individual member |
| Round GPS coordinates | Individual member |
| Entry challenge verifier | Individual member |
| Moderator directives | Recipient member or all members, as applicable |
generateRoundPackages(game_id):
game = loadGame(game_id)
members = loadActiveMembers(game_id)
locations = loadApprovedLocations(game.game_zone_id)
for member in members:
route = buildRoute(
member_role = member.role,
locations = locations,
round_count = game.round_count,
movement_rules = game.rules
)
package = createSignedBootstrapBundle(
game = game,
member = member,
route = route,
role_certificate = member.role_certificate
)
storePackage(member.game_member_id, package)
Telemetry supports operational visibility. It must not become a hidden real-time adjudication engine.
A player may be removed by an authorized moderator or administrator.
The server is authoritative for the existence of a boot directive. The server is not automatically authoritative for a player’s routine GPS position or Bluetooth interactions.
boot_pending.GET /membership-status.spectator, booted, or terminated.The backend receives final results after game completion or after the app reconnects.
The server stores outcomes and audit evidence but does not retroactively rewrite normal local play unless a configured moderation review determines that the game must be invalidated.
{
"game_id": "uuid",
"game_member_id": "uuid",
"final_state": "survived",
"rounds_completed": 5,
"correct_guesses": 4,
"incorrect_guesses": 2,
"local_event_ledger_root_hash": "hash",
"role_receipt_hashes": [
"hash-1",
"hash-2"
],
"submitted_at": "2026-06-20T20:05:00Z"
}
A creator may:
A moderator may:
An administrator may:
RBAC exists only to support game lifecycle control, moderation, venue authorization, and auditing. It is not a broad organizational access-control system.
| Entity | Core Purpose |
|---|---|
users |
Persistent authenticated player identity |
game_profiles |
Gameplay traits and photo-verification status |
devices |
App installation, capability, and optional push registration |
green_lights |
Venue authorization records |
game_zones |
Approved venue zones and safe location nodes |
games |
Game metadata and lifecycle state |
game_members |
Game-specific pseudonymous player membership |
game_configurations |
Server-approved configuration |
role_assignments |
Private role assignments and certificate references |
location_packages |
Signed offline package metadata |
telemetry_events |
Operational device telemetry |
moderation_actions |
Removal, pause, resume, and termination events |
moderation_directives |
Signed directives delivered to apps |
game_results |
Final player and game outcomes |
game_audit_records |
Event-ledger digests and anomaly flags |
The backend should not store by default:
| Function | Inputs | Primary Output |
|---|---|---|
validateGreenLight() |
Token, template, player identity | One-time creation grant |
createGame() |
Creation grant, approved configuration | Draft game and join code |
joinGame() |
Game ID, join code, player identity | Game membership |
updateGameConfiguration() |
Creator request, venue policy | Approved configuration |
startGame() |
Game ID, initiator identity | Active game or validation failure |
assignRoles() |
Active member list, role policy | Private assignments |
createRoleCertificate() |
Game member, assigned role | Signed role proof |
assignLocations() |
Members, approved nodes, role rules | Per-player routes |
createBootstrapBundle() |
Assignment, route, rules | Signed offline package |
getBootstrapBundle() |
Player membership | Member-specific package |
receiveTelemetryBatch() |
Operational event batch | Receipt and directives |
issueBootDirective() |
Moderator action | Signed removal directive |
confirmMembershipStatus() |
Player request | Authoritative active or removed state |
recordResults() |
Final result and ledger digest | Persisted outcome |
generateGameSummary() |
Game ID | Final summary |
reviewGameAudit() |
Game ID, moderator/admin identity | Audit summary |
terminateGame() |
Authorized moderation action | Terminated game state |
The backend must:
The design is considered complete for an initial implementation when the following conditions are met: