How a place runs
Enabling a draft pushed by place-kit isn't in the app yet. Drafts written in the app can be enabled today.
This page explains the model behind place code. No steps; for those, see the quickstart.
A place
A place is a room players walk into: a building on the map, or a virtual room. It has an owner, pieces (3D models on the floor, pictures and videos on the walls), doors, and the players inside.
Your code never sees names or positions of players. It sees player ids: strings to compare and store, never to parse. They will become 16-character public ids.
Two keys, two jobs
| Looks like | What it is | Who sees it | |
|---|---|---|---|
| Place key | S7kQ2xR9mPa | The public name of a place. place.json holds it. | Anyone. It's in share links. |
| Personal key | hwk_ + 43 characters | Permission to upload code to the places it names. | Only you and your agent. Shown once. |
A place key starts with S, V or P and has 10 to 20 characters. Your personal key is issued in the app, on a few of your places, for a few days, and revoked from the same sheet.
Two parts, two pipes
A place's code has up to two parts. Each part is one file with one class.
The server part (SpaceScript from @hatchworld/space@1) is the authority. It reacts to events, keeps state, decides, and tells the phones. It's required.
The client part (SpaceClient from @hatchworld/space-client@1) runs on each phone inside. It draws: buttons, labels, moving things. It decides nothing. It's optional: phones already show the server's ui.say plates.
They talk through exactly two pipes:
client this.net.action(name, target, payload) ──▶ server onPlayerAction(e)
server this.emit(name, payload, { to }) ──▶ client onServerEmit(e)Never trust the client. The server checks who acts (e.uid, e.isOwner) and its own state before changing anything.
Why your server class is new on every event
The server creates a fresh instance of your class for every event, then throws it away. A field you set in one handler is gone in the next. What must last goes in:
this.state: the place's memory, JSON, up to 64 KB;e.player.state: per player per place, up to 4 KB.
This is what lets a place sleep between events. It also means a handler may run twice for one event after a crash: check state before acting, so a second run does nothing new.
The client part is the opposite: one instance per visit. Its fields last while the player is inside and die when they leave.
Time and randomness
There's no setTimeout, fetch or eval. A timer is a name and a JSON payload, never a closure: this.runTimeout('close', 30000, { id }) comes back as onTimer(e).
Inside a handler, Date.now() is the event's time and Math.random() is seeded. Replaying an event gives the same result.
The manifest is your class's statics
Static fields of the server class declare what the place may do. The server reads them when you push:
| Static | Declares |
|---|---|
capabilities | Rights you use: world.write, llm, shared, notify. A call without its right is dropped. |
texts | Every text a player sees, by key, in English and at least one more language. |
commands | Phrases a player may type into the place's field, each arriving as an action. |
assets | The only model files the code may stand in the room, by hash. |
shared, boards, thresholds | Counters and leaderboards shared by the whole place. |
The full list with bounds is in the reference, generated from the types.
Draft, then enable
Nothing you push runs until the owner enables it.
your folder ──build──▶ dist/ ──push──▶ draft version N ──Enable (owner, in the app)──▶ liveplace-kit buildbundles and type-checks on your machine, the same way the server will.place-kit pushuploads both parts. The server checks them again (stricttsc, then a compile run in the sandbox) and saves version N as a draft.- The owner enables version N in the app. From then on it runs for everyone who walks in. A new version stops the old one's looks, music and highlights.
A personal key can carry the right to enable versions too, but only if the owner ticks "The agent may enable versions" when issuing it. By default it can't.
The sandbox and its limits
Your code runs in QuickJS: no network, no files, no DOM, no other imports. Each built part may import only its own API module.
| Limit | Value |
|---|---|
| CPU per event | 50 ms |
| Memory per event | 16 MB |
| Server part | 64 KB |
| Client part | 64 KB, more on request (up to 512 KB) |
this.state | 64 KB JSON |
e.player.state | 4 KB per player per place |
| Players in a room | Write for up to 24 at once |
A script that keeps failing or runs slow is switched off, and the owner sees why. All limits: Keys and limits.
What the platform already does
A system script runs before yours in every place. It greets newcomers, tracks who's inside, lets the owner throw a guest out, and lets guests borrow things for 24 hours. Don't rebuild these, and don't use the reserved ids take and kick.