# Tinyworld Integrations

> Use when changing Tiny World Builder API, webhook, SSE, MCP, plugin, or automation examples.

- Skill: `jasonkneen/tinyworld-integrations` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jasonkneen/tinyworld-integrations`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jasonkneen/tinyworld-integrations/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: jasonkneen (https://skillmd.com/u/jasonkneen)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jasonkneen/tinyworld-integrations

---


# Tiny World Integrations

The app has browser-local integration points plus a small Netlify account
backend:

- Account/profile/cloud-save functions live under `netlify/functions/`.
  `profile.mjs`, `builds.mjs`, `share.mjs`, and `assets.mjs` are routed to
  `/api/profile`, `/api/builds`, `/api/share`, and `/api/assets` via each
  function's exported `config.path`.
- Auth helpers should resolve the trusted site/Identity base from the same
  deploy-origin chain used elsewhere, including `TINYWORLD_SITE_URL`, before
  Netlify deploy URL fallbacks. Do not derive Identity verification targets from
  request-controlled origins.
- PartyKit durable flush buffers must clear every successfully-posted pending
  bucket in the `res.ok` branch (resources, tax payouts, GOLD events, etc.) so
  retries do not duplicate already-granted durable rewards.
- Worlds MMO grid size: the saved world payload (`data.gridSize`) is authoritative. Treat `worlds.grid_size` as cached metadata that can be stale; DTOs, previews, pricing/count derivation, and room entry should prefer/sync the payload size so an 8x8 map is not shown as 20x20 in multiplayer.
- Wallet/social functions also live under `netlify/functions/`: `wallet.mjs`
  verifies Phantom-signed Solana wallet challenges and reads `$TINYWORLD`
  balances/activity from RPC, `wallet-payments.mjs` creates Solana Pay payment
  intents, `players.mjs` tracks online presence/search/chat requests/parties,
  and `livekit-token.mjs` issues LiveKit room tokens when `LIVEKIT_URL`,
  `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET` are configured.
- Community functions: `community.mjs` (`/api/community`) backs the `/community`
  Discord-lite page — rooms, DMs, members, bans, blocks, invites; tables
  auto-create + seed on first request. Channel names are forced lowercase and
  the super-owner (`TINYWORLD_COMMUNITY_OWNER`, default `jasonkneen`) is made an
  owner of every room each request via `ensureCommunityDefaults`. Only staff
  (super-owner / `TINYWORLD_COMMUNITY_STAFF`) or a room owner can create/delete
  channels and ban; the same `admin` flag gates every privileged action.
  Members must pass an anti-AI human check (`community_verifications`) AND have a
  mandatory **Twitter/X handle** on their profile (GitHub optional) before they
  can post/DM/join — `saveSocials` writes the bare handles to
  `profiles.twitter`/`profiles.github` (idempotent `ALTER TABLE ... ADD COLUMN`
  in `ensureTables`; migration `20260615020000_add_profile_socials.sql`).
  Bootstrap returns `me.profileComplete`; the page shows a forced
  "Complete your profile" modal until Twitter is set and renders both handles on
  profile cards. Members can fully **edit their profile** (display name, bio,
  avatar, handles) via `saveProfile` (alias `saveSocials`). Avatars are an
  **allowlisted preset set** under `assets/avatars/*.png` (keys in
  `AVATAR_KEYS`) — no user image uploads, so no NSFW image risk. All
  user-authored text (display name + bio) is run through `checkTextSafety`,
  a two-layer filter (hard substrings + whole-word, leet/spacing-normalized)
  that rejects sexual / nudity / abusive / hateful content. Tested in
  `tests/community-profile.test.mjs`. `community.html` signs users in **in-page** (no bounce to the
  builder): it loads `vendor/tinyworld-auth.js` via the import map for Netlify
  Identity email login/signup and calls `/api/wallet` for Phantom login, storing
  the session under the shared `tinyworld:auth:wallet-session.v1` key.
- Community moderation webhook: `community-webhook.mjs` (`/api/community/webhook`)
  is a server-to-server endpoint for an agent (Hermes) to ban/unban/block/hide or restore/delete
  messages/purge spam/delete rooms. Auth is a shared secret
  (`TINYWORLD_COMMUNITY_WEBHOOK_SECRET`) via `x-tinyworld-signature: sha256=<hmac
  of raw body>` (preferred) or `x-webhook-secret`. Shared primitives live in
  `lib/community-moderation.mjs`; `community.mjs` also emits outbound
  `message.created` events to `HERMES_COMMUNITY_WEBHOOK_URL` (signed, fire-and-
  forget) so the agent can observe and react. Full reference:
  `docs/community-webhook.md`.
- User auth is Netlify Identity. The browser bridge is self-hosted through
  `vendor/tinyworld-auth.js` with an import map to vendored
  `@netlify/identity` / `gotrue-js`; do not reintroduce a remote identity
  widget script.
- The builder should not show working-looking account UI on hosts that cannot
  serve Netlify Identity. Treat 404/405 `/.netlify/identity/*` failures from
  the browser `getSettings()` probe or login calls as "auth unavailable": hide
  sign-in/account commands, keep local/static building usable, and leave cloud
  save/share/collab actions gated off. For local account work, use Netlify dev
  at `http://localhost:8888/tiny-world-builder`.
- Profile image fields stored through `/api/profile`, `/api/admin-users`, or
  community preset-avatar saves must be absolute `http(s)` URLs. Preset avatar
  paths under `assets/avatars/*.png` are normalized with the trusted site origin
  from `TINYWORLD_SITE_URL` / Netlify `URL` / deploy URL envs before validation
  and persistence; already-absolute URLs are left unchanged.
- Account API fetches must send `Authorization: Bearer <nf_jwt>` when possible
  and `credentials: 'same-origin'` so Netlify Functions can resolve the current
  Identity user. Wallet login uses the same bearer path with signed
  `tw-wallet-v1...` session tokens stored under `tinyworld:auth:*`.
- For local account/function work, run `npx netlify dev` and use
  `http://localhost:8888/tiny-world-builder`; that port keeps the auth/account
  UI enabled while the plain static dev server remains anonymous.
- Cloud worlds are stored as full TinyWorld JSON in Netlify Database `builds`
  rows. Existing rows update through `PUT /api/builds?id=<id>` so named
  localStorage worlds can stay bound to one cloud row instead of creating
  duplicates. Public share links create immutable-ish rows in `world_shares` and
  load through same-origin `?share=<id>` / `/api/share?id=<id>`.
- Multiplayer/shared building uses PartyKit separately from Netlify Functions.
  `partykit.json` points at `party/index.js`, local development runs with
  `npm run party:dev` on port `1999`, and browser rooms connect only when a URL
  includes `?party=`, `?room=`, or `?collab=`. Collaborate links should reuse a
  `/api/share` id as both the world snapshot id and the PartyKit room id:
  `/tiny-world-builder?share=<id>&party=<id>`.
- Shared build/collab rooms are public-observer by default: second and later
  PartyKit connections are admitted as `viewer` seats, not held in a lobby.
  Host clients heartbeat public room metadata to `/api/collabs`; the home page
  feed and `/collabs` page list those rooms with observer links
  (`observe=1`). This public visibility must not grant edit authority; edits
  still require a host-assigned role plus server-side island/zone checks.
- Closing a shared build is a two-layer operation: host clients send
  `room.close` to PartyKit so every connected peer receives `room.closed` and no
  replacement host is promoted, and they POST `{ action: 'close', roomId }` to
  `/api/collabs` so the public registry stores a short-lived tombstone in
  `collab_room_closures`. Heartbeats for tombstoned rooms must return
  `{ closed: true }` instead of recreating the listing.
- Admin collab moderation lives on `/collabs`: authenticated world-admin
  sessions call `/api/collabs` with `{ action: 'hide', roomId }` to add a
  short-lived `collab_room_hides` tombstone that removes a room from public
  lists without disconnecting occupants, or `{ action: 'adminClose', roomId }`
  to use the close tombstone. When a host sees that close tombstone on its
  registry heartbeat, the client must send PartyKit `room.close` before closing
  its socket so connected peers get the same shutdown event as a manual host
  stop.
- Shared build owners are tracked from the `/api/share` row. `/api/collabs`
  copies `world_shares.owner_auth_id/profile_id` into `collab_rooms`, exposes
  `GET /api/collabs?mine=1` for the builder world-menu "Shared rooms" section,
  and lets the owner/admin `hide` (make private), `unhide`, or `ownerClose`.
  `GET /api/collabs?roomId=<id>&control=1` can return a signed
  `tinyworld-collab-control` token; the builder sends `control.claim` to
  PartyKit so the original sharer/admin can reclaim host controls when reopening
  their own room link instead of staying an observer.
- Collaborative build zones are transient PartyKit room permission data, not
  saved world cells. Host clients send `zones.set`; the server sanitizes zones,
  stores editor `zoneIds`, and must gate every non-host `cell.set` against
  assigned active zones. Client outlines/labels and local edit checks are UX and
  desync prevention only; do not rely on them as the authority.
- MMO economy/multiplayer extraction lives in `packages/tinyworld-mmo-core/`.
  It is a dependency-free ESM package for shared GOLD allowance, resource tax,
  ledger, join-command, and interest-snapshot contracts. Use it when wiring the
  TinyWorld economy guide into PartyKit or Netlify Functions instead of copying
  constants between runtime files.
- Tinyverse published-world navigation no longer uses the `tinyverse-nexus` hub.
  `/api/worlds` should hide that slug from lists and direct loads, published
  world data should normalize to one center `stargate` with
  `dest: '__world-picker'`, and PartyKit `safeSpawn()` should prefer that gate
  so players arrive where the in-world picker exit is.
- Tinyverse/lobby access is locked to the Jason account allowlist in
  `netlify/functions/lib/tinyverse-access.mjs`. Do not use
  `accountMeetsCriteria()` or a raw `profiles.lobby_access` flag as the
  authoritative gate; migrations should keep `lobby_access` default false and
  clear it for every non-allowlisted profile.
- Tinyverse room join/refresh payloads use compact cells. Terrain-only cells may
  be `[x,z,terrain]`; object/resource cells are `[x,z,terrain,kind]`. Keep the
  renderer validator and `applyState()` tolerant of both tuple lengths.
- Explicit resource-bearing custom assets use object-form cells with
  `economy: { resource, charges?, label? }`. Live resources are currently
  `fish`, `ore`, `plants`, and `meat`; normalize through
  `packages/tinyworld-mmo-core/normalizeWorldResourceSpec(...)` in PartyKit and
  Netlify code instead of inferring resources from visual materials or copying
  constants. Compact tuple cells remain the default for ordinary terrain/kind
  saves.
- Tinyverse multiplayer rooms are runtime/play/moderation surfaces only. Do not
  add live island building controls, `adminSave`, build-role seats, or
  `world.refresh` board replacement inside PartyKit rooms. Island editing and
  version publication must live in the dedicated draft/version flow.
- Local custom assets are account data too: `/api/assets` stores one
  `asset_libraries` row per profile containing custom voxel-build stamps and
  saved asset templates. Browser hooks in `saveCustomVoxelBuildStamps()` and
  `saveAssetTemplates()` queue a cloud sync after login.
- Local Netlify Database failures are expected in some `netlify dev` sessions.
  Translate 503 `Netlify Database is not available...` responses into a friendly
  account/cloud status or `warn` toast, never a red production-style error toast,
  raw database message, or visible `Local DB offline` wording.
- Wallet/player social functions rely on
  `netlify/database/migrations/20260602120000_wallet_players_social.sql`.
  If those tables are missing in local Netlify dev, classify Postgres `42P01`
  with `isMissingRelations(...)` and return a setup-oriented 503 instead of
  logging raw missing-relation errors as generic 500s.
- Phantom wallet linking and wallet login must stay challenge/response based:
  the browser asks Phantom to sign the server-issued message and the function
  verifies the Ed25519 signature against the Solana public key before linking
  or minting a wallet session. Do not accept a posted wallet address as proof
  of ownership. Wallet login requires `TINYWORLD_WALLET_SESSION_SECRET` (or
  `TINYWORLD_AUTH_SECRET`) for HMAC-signed challenge/session tokens.
  `$TINYWORLD` mint/payment values come from env (`TINYWORLD_TOKEN_MINT`,
  `TINYWORLD_PAYMENT_WALLET`, optional `SOLANA_RPC_URL`) rather than client
  constants.
- Database schema changes belong in `netlify/database/migrations/*.sql`. Deploy
  previews get their own database branch, so use a preview deploy for real
  Identity + DB verification; local `netlify dev` is useful for functions but is
  not a complete Identity social-login test.

Browser-local integration points:

- Outbound webhooks live in `tiny-world-builder.html` under
  `// -------- API / webhooks / SSE bridge --------`.
- Optional browser-local probes must be opt-in so the static app stays console-clean:
  the Cluso in-page embed is LOCAL-DEV-ONLY, injected at runtime by `tools/dev-server.js`
  (assets in gitignored `cluso/`); it must never be referenced by committed/shipped HTML;
  model-stamp API endpoints load only with `?modelApi=1`, `?modelStampApi=1`,
  `window.__TWB_MODEL_STAMP_API_ENABLED__ = true`, or
  `localStorage['tinyworld:features:model-stamp-api']='1'`.
- `fireWebhook(event, payload)` batches editor mutations and POSTs
  `{ source: 'tiny-world-builder', events }` to the configured Developer-panel
  webhook URL.
- Inbound automation uses `EventSource` against the configured Developer-panel
  SSE URL. Each SSE `data:` payload must be one JSON command accepted by
  `applyRemoteCommand`.
- Supported inbound ops include `place` / `set_cell`, `clear`, `reset`, plus runtime-only vehicle controls: `vehicle_spawn`, `vehicle_set_goal`, `vehicle_controls`, `vehicle_remove`, and `vehicle_clear`.
- Runtime vehicles must not pass through each other. Keep traffic behavior in the runtime layer: collision radius + yield radius, brake when another vehicle is inside the envelope, and reroute around occupied road cells after a short blockage when an alternate road path exists.
- Placed objects on paths are live traffic blockers. `isVehicleDrivableCell` should allow path cells only when the main `kind`/extras do not occupy the tile, while bridge cells remain drivable. Call `refreshVehiclesForWorldObstacleChange` from world edit paths so active auto vehicles reroute immediately when the user drops or removes an obstacle.

Examples live under `plugins/examples/`:

- `webhook-receiver.js` captures outbound webhook batches.
- `sse-command-relay.js` exposes `/sse` for the browser and `/command` for
  external clients.
- `send-command.js` is a small CLI for the relay.
- `mcp-stdio-bridge.js` is a dependency-free MCP stdio server that calls the
  relay and reads the webhook log.
- `vehicle-road-demo.js` is a dependency-free MCP client/demo runner that talks
  to `mcp-stdio-bridge.js`, paints a visible road/water/bridge network, spawns
  runtime vehicles, and retargets them in a loop so the browser remains
  watchably active.
- The app also supports browser-native shareable vehicle demo URLs:
  - `?demo=vehicles&seed=tide-ridge-428` creates the small/default visible road demo.
  - `?demo=vehicles-large&seed=metro-culdesac-20&stats=1` creates the default 20×20 scale test with arterial/ring roads, bridge crossings, cul-de-sac endpoints, and 36 autonomous vehicles on long routes.
  - Large-demo params: `size=` / `mapSize=` / `grid=` / `gridSize=` accept the nearest valid demo grid size from `12` through `20` (`12`, `16`, `20`); `cars=` / `carCount=` / `vehicles=` / `vehicleCount=` accept `1..120` and are capped by available unique endpoints.
  Keep these demos visually self-identifying: show an active badge, hide overlays
  that cover the road network, and make vehicles obvious with beacons/markers.
  During local demo work, `tools/dev-server.js` should make bare
  `http://localhost:3000/` and no-query `http://localhost:3000/tiny-world-builder`
  redirect to the small seed so the user can simply open the port or remembered
  app URL and watch it. Use the large URL explicitly for scale/perf checks.

When changing command shape, update the app bridge and these examples together.

