| name | tinyworld-integrations |
| description | Use when changing Tiny World Builder API, webhook, SSE, MCP, plugin, or automation examples. |
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.