In a shared AR scene, placing the same block on two phones is only half the problem. The relay also needs a clear answer to a less visible question: who is allowed to change that block? I traced that request path in Shared AR Workshop and found a useful distinction between limiting what a request can do and deciding who should be allowed to send it.
Shared AR Workshop is a Unity AR prototype with a small Node.js relay. Each phone tracks its own camera and room locally, then sends block transforms to the relay. That keeps camera frames and room scans off the server, but it does not automatically give participants separate identities or roles.
SHARED AR WORKSHOP / REQUEST PATH
Room membership is the current trust boundary- 01 / JOINRoom code
POST /joinThe relay creates the room and returns a participant ID.
- 02 / PROVE MEMBERSHIP
clientIdURL + JSON bodyThe relay checks that this ID exists in the room.
- 03 / CHANGE STATEAny member
upsert · delete · pollThere is no owner role or per-object permission check.
Align two local worlds by hand
ARKit and ARCore each track a device in its own coordinate system. A position measured by one phone is not automatically meaningful in the other phone’s world. In this prototype, each participant points at the same physical origin and a second point to define a forward direction. The app creates a local anchor and stores block transforms relative to it.
The relay sees room codes, display names, object IDs, transforms, colors and physics flags. The project documentation says camera frames and room mapping stay on the device. The approach makes a small collaboration flow possible without uploading the room itself, while leaving alignment accuracy dependent on both people choosing the same points. It is a manual coordinate convention, not a shared cloud anchor or a measured positioning result.
Follow the join before calling it authorization
The server’s join route creates the room when it does not exist and returns a random participant ID. For later polls, the Unity client puts that ID in the event URL; for edits, it puts the ID in the JSON body. The relay checks whether that value is present in the room’s member map.
That check is useful: a request with an unknown ID is rejected. But it is not the separate Bearer-token design described by the repository’s top-level README. At the reviewed revision, the relay does not issue an accessToken, inspect an Authorization header, or implement the documented batch and replace routes. Its server-specific README instead describes the room code as granting edit access. The code and docs need to agree before someone relies on either description.
There is also no room owner or object owner in the request path. A member who can edit can upsert or delete any object ID in the room. The eight-character code generated by the Unity interface is a convenient invitation to share with another participant; it is not a user account, role system or moderation boundary. The clientId is the current membership credential, and because polling sends it in a URL, a hosted deployment should consider whether proxy or access logs retain query strings.
Bound the work a request can cause
The relay does several practical things to keep a prototype’s state finite. It caps JSON bodies at 16 KB, checks positions and rotations for finite values and allowed ranges, normalizes quaternions, restricts palette indexes, and limits a room to 200 objects and 32 members. Writes are limited to 40 per second per member. A room retains up to 4,096 events, and an individual poll returns at most 256 before the client asks again.
Each accepted change gets an increasing sequence number. If a client’s cursor is older than the retained event history, the relay returns a fresh object snapshot instead of pretending it can still replay every event. Long polls wait up to 20 seconds, with a per-member pending-poll limit and a global cap. These are useful resource and input controls; they do not tell the server which participant owns a block.
One implementation detail is worth tightening: the current handler refreshes a room’s activity timestamp before it validates membership on non-join routes. That means an invalid request to an existing room can keep the room from reaching its inactivity cleanup. The top-level README says unauthenticated requests do not keep rooms alive, but this source path does not enforce that claim.
What I verified, and what I did not
I reviewed the public repository at commit e7fbd96. On Node.js 24.12.0, npm run check passed and npm test passed all four tests. They exercise health and join-name validation, room membership and event delivery, bad cursors and oversized input, and cleanup after an aborted long poll. This is fresh evidence for the relay checks I ran, not a claim that every guard or recovery path has a dedicated test.
The same repository says its Unity source has not been compiled in the editor and that there is no generated APK or recorded two-device session. I did not verify AR tracking, two-phone alignment, reconnect behavior, or end-to-end synchronization on hardware. The architecture below the source is therefore a code review of a prototype, not a device demonstration.
You can inspect the relay implementation, relay tests, Unity network client, and architecture notes. The repository is public, so these claims can be checked against the code.
The next change is to make the contract match
Before opening a relay to public traffic, I would decide whether room membership is intentionally the whole access model or whether joining should require a separate invitation secret. Then I would put any credential in an authorization header, define whether participants can edit every block, move activity refresh after successful authorization, and update the README to describe only implemented routes. Tests should cover those choices alongside stale-cursor resynchronization and rate limits.
In this relay, the 16 KB body cap and 40-writes-per-second limit constrain request cost; the room’s member ID is the only current edit check. That ID is carried in request data, and any member can modify any object in the room. Before public use, the implementation and README need one matching access contract, with credentials out of URLs and tests for unauthorized edits and room cleanup. For related work on network state, read how a Unity client can reconcile predicted motion and how FieldCore handles an exact sync retry.