Home Grids API

Every player gets one free home grid — a personal plot-sized zone (64×64×64) they fully control. Home grids are player-hosted: the owner's own game client runs the world as a listen server, with no dedicated server involved. Blocks persist to the cloud through the standard block endpoints, authorized by the owner's user JWT.

Key properties:


Provisioning

Get (or Create) My Home Grid ✅ Auth

GET /api/zones/home

Returns the caller's home zone. If it doesn't exist yet, it is created on the spot — free of charge — along with two default roles: Guest (no permissions) and Builder (zone.build).

Response (200 existing, 201 newly created):

{
  "id": "zone_abc123",
  "owner_id": "user_xyz",
  "name": "willow's Home",
  "size_tier": "plot",
  "coord_x": 42,
  "coord_y": 1000000,
  "width": 64, "height": 64, "depth": 64,
  "visibility": "invite_only",
  "max_players": 8,
  "zone_type": "home",
  "created": false
}

Resolve Another Player's Home ✅ Auth

GET /api/zones/home/of/:userId

Returns basic info about a player's home zone, including whether it is currently being hosted.

{
  "id": "zone_abc123",
  "name": "willow's Home",
  "owner_id": "user_xyz",
  "owner_name": "willow",
  "visibility": "invite_only",
  "max_players": 8,
  "is_hosted": true
}

Errors: 404 (player has no home zone)


Host Sessions

Because home grids have no dedicated server, the owner's client announces a host session while it is hosting. Guests can only join while a session is live.

All three endpoints require the zone owner's user JWT and a zone_type = 'home' zone.

Start Hosting ✅ Auth (Owner Only)

POST /api/zones/:id/host/start
{
  "eos_session_id": "abc...",     // optional — EOS session for P2P join
  "connect_string": "1.2.3.4:7777", // optional — direct connect fallback
  "max_players": 8                 // optional, capped at the zone's max
}

One session per zone — restarting the host replaces the previous session.

Heartbeat ✅ Auth (Owner Only)

POST /api/zones/:id/host/heartbeat
{ "player_count": 3, "eos_session_id": "abc..." }

Send every ~30 seconds. Sessions with heartbeats older than 2 minutes are considered offline (a cron sweep marks stale sessions offline every 5 minutes).

Errors: 404 (no session — call /host/start first)

Stop Hosting ✅ Auth (Owner Only)

POST /api/zones/:id/host/stop

Marks the session offline immediately.


Joining a Home Grid

The standard Join Zone endpoint branches for home zones:

GET /api/zones/:id/join

Owner — always receives a directive to self-host:

{
  "zone_id": "zone_abc123",
  "zone_name": "willow's Home",
  "connect_method": "host_local",
  "width": 64, "height": 64, "depth": 64,
  "size_tier": "plot",
  "max_players": 8
}

Guest, owner hosting — receives the live session plus a visit token:

{
  "zone_id": "zone_abc123",
  "connect_method": "eos",
  "eos_session_id": "abc...",
  "connect_string": "1.2.3.4:7777",
  "visit_token": "eyJ...",
  "player_count": 2,
  "max_players": 8
}

The visit token's can_build claim is resolved from the guest's zone roles — a guest with the Builder role can place and remove blocks; everyone else is visit-only.

Guest, owner offline503:

{ "error": "The owner is not hosting this home grid right now" }

Standard ban and visibility checks apply before any of the above (invite-only means guests need a membership from a redeemed invite).


Invites

Invite codes grant membership to non-public zones — including home grids. Schema supports both open codes and codes targeted at a specific player.

Create Invite ✅ Auth (Owner or zone.members)

POST /api/zones/:id/invites
{
  "invited_user_id": "user_abc",  // optional — restrict to one player
  "max_uses": 5,                   // optional — default unlimited
  "expires_in_hours": 24           // optional — default never
}

Response (201):

{
  "id": "invite_1",
  "zone_id": "zone_abc123",
  "invite_code": "7f3kq9xd",
  "max_uses": 5,
  "expires_in_hours": 24
}

Redeem Invite ✅ Auth

POST /api/zones/invites/:code/accept

Adds the caller to the zone with the Guest role (created on demand if missing).

{ "zone_id": "zone_abc123", "zone_name": "willow's Home", "joined": true }

Errors: 404 (unknown code), 410 (expired or no uses remaining), 403 (targeted at another player, or banned)

List Invites ✅ Auth (Owner or zone.members)

GET /api/zones/:id/invites

Revoke Invite ✅ Auth (Owner or zone.members)

DELETE /api/zones/:id/invites/:inviteId

My Memberships ✅ Auth

GET /api/zones/memberships

Returns the zones the caller has joined — the "places I can visit" list.

{
  "zones": [
    {
      "id": "zone_abc123",
      "name": "willow's Home",
      "zone_type": "home",
      "owner_id": "user_xyz",
      "owner_name": "willow",
      "size_tier": "plot",
      "visibility": "invite_only"
    }
  ]
}

Block Persistence (Owner JWT)

The block endpoints normally require an operator key (dedicated servers only). For home zones, they also accept the owner's user JWT — the owner's client is the hosting authority for their own home:

POST   /api/zones/:id/blocks            Authorization: Bearer <user JWT>
DELETE /api/zones/:id/blocks/:blockId
PUT    /api/zones/:id/blocks/:blockId/faces
GET    /api/zones/:id/blocks/all
POST   /api/zones/:id/chat

The JWT path is rejected (403) unless both are true:

  1. The zone's zone_type is home
  2. The JWT subject is the zone's owner

Guests' builds are persisted by the hosting owner's client with the guest recorded as placed_by.


Game Client Flow

  1. Player clicks My Home Grid in the main menu
  2. Client calls GET /api/zones/home (creates the zone on first use)
  3. Client opens the home level as a listen server (AHomeGridGameMode) — the host's PlayerState gets full owner permissions from the local session
  4. Client calls POST /:id/host/start and heartbeats while hosting
  5. Persisted blocks load via GET /:id/blocks/all; new builds save through the block endpoints with the owner's JWT
  6. Guests with a redeemed invite call /join and connect to the host's session, authenticated by their visit token on PreLogin
  7. When the owner leaves, the client posts /host/stop and guests are disconnected