Skip to main content
Version: 3.4.8 (unreleased)

Multiuser Virtual Field Trips & Training

The Virtual Field Trip (VFT) system lets several people share a single VRGS scene in real time — walking the same outcrop, looking at the same models, and collaborating through pointers, annotations, chat, and shared waypoints. It is designed for teaching, guided field trips, and training, where one instructor (the leader) drives a session that students or trainees join.

A live session is called a trip. One person hosts it; everyone else joins with an invite code and is admitted through a waiting room. While the trip runs, participants see each other's avatars and viewpoints, the leader can recall everyone to a location, presenters can drop annotations and waypoints, and the whole session can be recorded for later replay and debrief.

System at a glance​

The system has three parts:

ComponentWhat it isWho uses it
VRGS clientThe desktop / VR application. The VFT panel (a dockable side panel) is the control surface for hosting and joining.Instructors, students, trainees
geotour-serverA standalone networking backend (a Go WebSocket server). It hosts trips, manages rosters and waiting rooms, relays movement/chat/annotations, and stores history.Runs in the background / on a server; operators configure it
Admin dashboardA web app for monitoring live trips, users, recordings, audit history, and analytics.Administrators / instructors reviewing sessions

The VRGS client connects to the server over a WebSocket (ws://<host>:<port>/ws) and authenticates with a sign-in token. The server keeps the authoritative state — who is in which trip, what role they hold, and the shared annotations/waypoints — and broadcasts changes to everyone in the trip.

note

The networking backend is a separate program (geotour-server). VRGS does not embed it; an instructor (or your organisation's IT) runs one server that many clients connect to. See Running the server.

Key concepts​

  • Trip — a live collaborative session. It has a name, an optional password, a participant cap, a leader, and a roster of everyone currently in it. A trip is active (running now) or scheduled (set to start at a future time).
  • Invite code — a short code (derived from the trip's id) the host shares so others can join.
  • Waiting room — when someone asks to join, they wait here until the leader or a TA approves them. This keeps uninvited people out of a class.
  • Owner — the person who created the trip. Ownership never transfers; the owner can always reclaim or reassign the Leader role, and while the owner is away no new people are admitted (see Ownership vs leadership).
  • Leader — the member currently driving the session. Leadership can be handed to another attendee and handed back. The owner is the initial leader.
  • Roles — every member also holds one role that decides what they can do (see the permissions matrix).
  • Presence / roster — the live list of who is in the trip right now, shown in the VFT panel.
  • Recall & control — the leader can pull participants to their location ("recall"), or hand a participant temporary control of the shared view.
  • Annotations & waypoints — shared spatial markers. Annotations are free notes pinned in 3D from the panel's Notes tab; they sync to everyone (and back-fill people who join later) and are drawn on the mesh in the 3D view. Waypoints are ordered stops (with optional dip/azimuth) that define a route through the scene.
  • Chat, whispers & resource sharing — text messages and shared links/resources, either to the whole trip or as a private whisper to one member.
  • Voice & video — everyone in a trip can talk, with push-to-talk or an open mic and optional distance-based spatial audio; the leader can also share a camera, shown in a single tile above the tabs. Both are live-only and are never recorded (see Voice chat and Video).
  • Signals & ink — lightweight ways to communicate without interrupting: raise a hand, send an emoji reaction, draw freehand ink on the outcrop, and (leader/owner) a "look here" 3D spotlight that marks exactly where to look.
  • Trips browser — the panel's Trips tab (and the Join dialog's Browse Trips) list available and scheduled trips, each scheduled one with a live countdown to its start.
  • Recording & replay — the leader can record a session; the server samples everyone's positions over time so the trip can be replayed afterwards for debrief.
  • Analytics — from recorded movement the system builds position trails and an attention heatmap (where people looked), viewable in the dashboard.

Roles & permissions​

There are five roles. The host of a trip is automatically the Leader; everyone admitted from the waiting room starts as a Participant. The leader can promote anyone to another role.

CapabilityObserverParticipantPresenterTALeader
See the shared scene✓✓✓✓✓
Broadcast their own avatar / movement—✓✓✓✓
Chat & share resources—✓✓✓✓
Speak, and hear others (voice chat)—✓✓✓✓
Turn their own camera on (video)—✓✓✓✓
Add annotations—✓✓✓✓
Remove annotations——✓✓✓
Add / remove waypoints——✓✓✓
Request control of the view—✓✓✓✓
Recall a single participant———✓✓
Approve / reject the waiting room———✓✓
Give control to a participant————✓
Recall everyone————✓
Mute one member, or mute everyone————✓
Turn another member's camera off (and allow it back)————✓
Change another member's role————✓
Start / stop recording————✓
End the trip————✓
  • Leader — the host. Full control of the session.
  • TA (teaching assistant) — helps run a class: can admit/reject people from the waiting room and recall an individual, plus everything a presenter can do.
  • Presenter — a participant who can curate the route: add/remove annotations and waypoints.
  • Participant — the default. Moves freely, chats, shares resources, and adds annotations.
  • Observer — a silent watcher. Does not broadcast a moving avatar and cannot chat, annotate, or share — useful for a guest who only wants to watch.
tip

All of these permissions are enforced on the server, and always against the member's own trip — a leader of one trip cannot affect another trip. The role shown in your VFT panel is the source of truth.

Ownership vs leadership​

The person who creates a trip is its owner — a status distinct from the transferable Leader role:

  • Leadership can move; ownership cannot. The owner is always the original creator. Leadership can be handed to another attendee (Make Leader) and handed back again, but ownership never transfers.
  • The owner can always reclaim or reassign leadership — even when the owner is not the current leader. Reclaim Lead takes leadership back; Make Leader hands it to any attendee. This is a safety net if a delegated leader misbehaves or drops out.
  • While the owner is away, no new participants are admitted. Existing members carry on and the trip keeps running, but the waiting room is closed until the owner returns. The VFT panel shows an "owner away" note while this is in effect.
  • The owner resumes ownership on reconnect. If the owner leaves and signs back in they automatically become owner again (matched by their sign-in identity; in a development setup without full sign-in, matched by display name).

The VFT panel tags the owner in the roster and shows the owner-only Reclaim Lead / Make Leader controls.

note

"Make Leader" / "Reclaim Lead" transfer the Leader role itself. This is separate from the transient give / request control of the shared view described below, which lets someone drive the camera while others follow without changing who the leader is.

Guests & who can join​

You do not need an account to attend a trip — but you do to host one:

  • Hosting requires sign-in. Only a signed-in user can create or schedule a trip, because the creator becomes the immutable owner (see above) and that must map to a stable identity. Trying to host without signing in is refused with "sign in to create a trip".
  • Guests can join. Anyone with the invite code can join as a guest without signing in. Guests take part like everyone else — move, chat, raise a hand, follow the leader — but can never own a trip. They are tagged Guest in the roster so the leader can see who is unauthenticated.
  • Admission is the owner's choice. When hosting, Require approval to join (on by default) governs how new participants — guests and signed-in attendees alike — enter. With it on, everyone waits in the waiting room until the owner or a TA admits them; with it off, they join straight away. The setting applies to the whole trip, not just guests.
note

An operator can disable guest access for a whole deployment (ALLOW_GUEST_CONNECTIONS=false), in which case every connection must be signed in. See the Configuration reference.

Architecture (how it fits together)​

VRGS client ─┐
VRGS client ─┤ WebSocket (binary protobuf) ┌── PostgreSQL (trips, participants,
VRGS client ─┼──────────────────────────────────►│ chat, annotations, waypoints,
VRGS client ─┘ ws://host:port/ws?token=… │ recordings, trails, audit)
│ │
geotour-server ──────────────┤
│ └── Valkey/Redis (live presence,
Admin dashboard ─────────────┘ rate limits, recording buffer)
(web) /api/v1 + /ws/dashboard
  • Each client opens one WebSocket and exchanges compact protobuf messages (player pose, chat, annotations, trip commands). Voice rides this same connection as Opus frames the server forwards without decoding them.
  • Video uses a second WebSocket to the same server (/ws/video), for one reason: a busy picture must never be able to hold up the voice. Encoded frames are forwarded, never transcoded. Because both ride ordinary secure WebSockets there is no UDP, no peer-to-peer and nothing extra for a firewall to allow.
  • PostgreSQL holds durable history (who joined which trip, chat, annotations, waypoints, recordings, movement trails, and the audit log).
  • Valkey (a Redis-compatible cache) holds fast-changing live state (current player positions, per-IP/per-client rate limits). It is optional — without it the server still runs, but presence-driven features (recording, analytics) and rate limiting are disabled.
  • The admin dashboard talks to the server's REST API (/api/v1/...) and a read-only live feed (/ws/dashboard).

Using VFT in VRGS​

1. Open the VFT panel and sign in​

Open the VFT panel in VRGS (a dockable side panel). The panel connects to the configured geotour-server and signs you in. Your display name and identity come from your sign-in account; in a development setup the server may accept any name without a full login.

If the panel shows that it cannot reach the server, the backend may not be running — see Troubleshooting.

The trip tool row​

Everything you do during a trip is a button in the row under the connection status, above the tabs. The buttons are all the same size and the row wraps onto as many lines as the panel is wide — dock the panel wider and you get more per line.

The row only appears while you are in a trip, and each button only appears when it applies to you, so you never see a control that would do nothing. A button that is a toggle lights up green when it is on, and its tooltip says which way it is set.

ButtonWho sees itWhat it does
✋ HandeveryoneRaise or lower your hand. A Hand tag appears by your name for the whole trip.
👀 Look Hereleader / ownerArms a pick — your next click in the 3D view marks the spot for everyone.
✏️ InkeveryoneArms the pen for freehand ink. Click again to put it away.
🧽 Clear Inkeveryone, only when there is inkClears the ink (see Freehand ink).
🔦 PointerseveryoneOff: only the leader's pointer is drawn. On: everyone's, each labelled with its owner.
📢 Recall AllleaderBrings every participant to your viewpoint.
🎥 Followeveryone except the leaderYour camera tracks the leader's as they move.
⏸ Pause / ▶ ResumeleaderPauses or resumes the trip.
🎙 Mic, 🎚 Open Mic / Push-to-Talk, 🔇 Mute All, 🔊 Unmute, 🧭 Spatialvoice buttons appear once the audio engine is running; Mute All / Unmute are leader-onlySee Voice chat.
🎥 Cameraeveryone, once the server is hosting videoTurn your own camera on or off. See Video — the camera tile.
🫧 Backgroundeveryone, only while your own camera is onChoose what appears behind you — blur, a green screen, or a flat colour.
🎨 Displayeveryone except the leaderFollow the leader's shading and colouring, or keep your own.
🚪 Exit TripeveryoneLeave the trip (you are asked to confirm).
note

These controls used to be split between a small icon toolbar on the panel and a button on the ribbon. They are all in this one row now — a trip control means nothing when no trip is running, so it belongs with the trip, not on the ribbon.

2. Host a trip​

  1. In the VFT panel choose Create / Host a trip. (You must be signed in — only an authenticated user can host; see Guests & who can join.)
  2. Give it a name, and optionally a password and a participant limit.
  3. Optionally enable auto-record so the session is captured from the start.
  4. Choose whether to Require approval to join (on by default). On means every new participant waits in the waiting room for you to admit them; off means they join straight away.
  5. Create it — you become the trip's owner and its first Leader, and the panel shows an invite code with a Copy button.
  6. Share the invite code with your group. The code stays visible in the panel for the whole trip, and any member can copy it to re-share.

You can also schedule a trip for later: tick Schedule for later when hosting and choose a start date and time. The trip is created but does not open a live session yet — it appears in the Trips tab with a live countdown and becomes joinable when its start time arrives (rejoin with the invite code when it begins). Scheduled trips persist on the server, so they survive a server restart.

3. Join a trip​

  1. Open the trip set-up dialog. It opens on Join Trip with the cursor already in the Invite Code box, because being handed a code for someone else's trip is the common case; switch to Create Trip at the top if you are hosting.

  2. Either type or paste the invite code (and the password, if the trip has one), or click Browse Trips to fetch the list of available and scheduled trips and pick one — its invite code fills in automatically. Add your display name and join. You can join without an account: if you are not signed in you join as a guest (see Guests & who can join).

    Codes are forgiving about how they arrive. Case does not matter, and stray spaces, line breaks, or separators picked up from an email or chat message (1ZK-Q4X, 1ZK Q4X) are ignored. If what you paste could not be a code at all, the dialog says so instead of leaving you waiting on a trip that does not exist.

    The other fields are Screen Name (the name everyone sees), Server (leave it at the default unless you have been given another address), and Password.

  3. If the trip requires approval (the default), you enter its waiting room and see your queue position; when the leader or a TA approves you, you join the live trip as a Participant. If approval is off, you join immediately. If you are rejected — or the wait times out — you are returned to the panel and can try again.

4. Admit people (leader / TA)​

As the leader (or a TA), the VFT panel shows the waiting room list. For each pending person you can Approve or Reject. Approving adds them to the roster (subject to the participant cap); rejecting (or a timeout) removes them from the queue and notifies them.

5. Collaborate during a trip​

Once people are in, the shared session supports:

  • Presence — everyone's avatar and viewpoint are visible (except observers), updated live as people move.

  • Recall — the leader can recall everyone to their current location with 📢 Recall All, and a leader or TA can recall a single participant from that person's row in the roster. Useful for "everyone come look at this".

  • Give / request control — a participant can request control of the shared view; the leader can give control to a participant so they can drive while everyone follows. (This is the transient view control — distinct from transferring the Leader role itself; see Ownership vs leadership.)

  • Notes (annotations) — open the Notes tab and Pin a note at your current viewpoint. Shared notes appear on the mesh in the 3D view and are listed for everyone (people who join later get the full list too). Each note has a Go To that flies your camera to it. Participants and above can add notes; presenters, TAs, and the leader can remove them.

  • Freehand ink — press ✏️ Ink to arm the pen, then press and drag on the outcrop to draw. The stroke appears as you draw it and is shared with everyone the moment you release, coloured per author. The pen stays armed for as many strokes as you want until you click ✏️ Ink again. Strokes are drawn as rounded 3D lines of constant on-screen thickness, so they stay legible whether the whole cliff is in frame or you are up against a single bed.

    A stroke belongs to the surface you started it on: if the pen slips off the outcrop — onto the sky, or onto the ground behind it — the line breaks there and picks up again when you come back, rather than stretching a segment across the scene.

    🧽 Clear Ink appears once there is ink to clear. What it clears depends on who you are: the leader clears the board for the whole trip, and anyone else clears their own view and withdraws their own strokes from everyone else's — one participant can never erase another's work.

    Ink is a live overlay — it is not saved into anyone's project, and it clears when you leave the trip.

  • Follow the leader — press 🎥 Follow and your camera tracks the leader's as they move, so you see what they see without chasing them. Press it again to move freely. While you are following, the leader's own avatar and the near end of their pointer beam are hidden, since your viewpoint is theirs and the model would otherwise be sitting on your camera — the marker at the far end of their pointer, which is the part that tells you where to look, stays put.

  • Whose pointers you see — 🔦 Pointers switches between the leader's pointer only (the usual "watch what I'm pointing at") and every participant's, each labelled with its owner's name.

  • Waypoints — presenters/TAs/leader can place ordered stops (with optional dip & azimuth) that define a route through the scene.

  • Chat & whispers — send messages to the whole trip, or pick a person from the recipient selector to send a private whisper (shown with a 🔒 marker to sender and recipient). Resource links can also be shared. (Messages are length-limited and links are restricted to http/https.)

  • Raise hand — any member can toggle ✋ Raise Hand in the panel to flag the leader; a Hand tag appears next to their name in the roster for everyone.

  • Reactions — send a quick emoji reaction (👍 👏 🎉 ❤️ ❓ 😮) that pops up in the panel for the whole trip — a fast, non-disruptive way to respond.

  • Look here — the leader or owner can press 👀 Look Here to pull everyone's focus. It flashes an attention banner for the whole trip and, when the leader is pointing at the mesh, drops a bright 3D marker at that exact point so everyone can see where to look (not just that they should).

  • Attention overview (leader) — the roster shows each participant's latency, whether their hand is raised, and how far they've drifted from the leader (with the furthest flagged) — so a guide can see at a glance who is keeping up. Anyone using 🎥 Follow shows a drift of zero, because their camera is the leader's; drift tells you about the people who have gone their own way.

  • Roles — the leader can promote/demote members (e.g. make a co-instructor a TA, or set a guest to Observer).

Voice chat​

Everyone in a trip can talk. There is nothing to set up and no separate call to join: the moment you are admitted to a trip VRGS opens your microphone and speakers, and it closes them again the moment you leave — the microphone is never held open outside a session.

Voice rides the same secure connection as the rest of the trip, so it works anywhere VRGS already connects: no extra ports, no peer-to-peer, nothing for IT to open on a hotel, campus or corporate network.

Talking​

ControlWhat it does
` (backtick)Push to talk. Hold it to transmit; release to stop.
🎙 MicMute or unmute yourself. While muted, holding the key sends nothing. The button turns red when you are muted — it is the one toggle whose "on" is a warning.
🎚 Open Mic / Push-to-TalkSwitch between transmitting whenever you speak and transmitting only while the key is held.
🔇 Mute All / 🔊 UnmuteLeader only. Mute or unmute everyone except the leader.
🧭 SpatialVolume falls off with the distance between avatars, or everyone at full volume.

In open mic the microphone opens by itself when you speak — holding the push-to-talk key still works as a force mic on top of it. Whichever mode you are in, the roster shows a speaking indicator against whoever is talking right now, and your own row tells you when you are live.

How many people can talk at once​

Up to six voices are carried at a time, and the leader is always one of them. Past four or five people talking together a conversation is unfollowable anyway, so in a normal trip you will never meet this. It exists for a big session on open mic, where microphones can be opened by room noise rather than by anyone actually speaking — without a limit, a hundred-person trip can spend its bandwidth on a hundred empty rooms.

Whoever started talking first keeps the floor, so you cannot be cut off mid-sentence by someone else joining in, and a place frees the moment somebody stops. If a large trip is talking over itself, 🔇 Mute All and hand-raise are still the tools for taking control of it.

Use headphones

There is no echo cancellation. On open speakers your microphone picks up everyone else and feeds it back into the trip. Headphones solve it completely, and in VR you are wearing them anyway. Push-to-talk is the other mitigation, which is why it is the safer default for a class.

Keyboard noise​

The noise a class actually complains about is typing, and a keystroke is louder than quiet speech — so a microphone gate that only measured loudness let it straight through. VRGS instead looks at the shape of each moment of sound: a keystroke is a single sharp impulse, speech fills its frame evenly and takes several frames to get going. Isolated clicks therefore neither open the microphone nor hold it open.

While you are holding push-to-talk the gate never stops you transmitting — it would not be push-to-talk if it did. It ducks the typing about 24 dB instead, which keeps your room tone continuous rather than sounding like a dropped connection between words.

Spatial audio​

With 🧭 Spatial on, a speaker's volume falls off with the distance between your viewpoint and their avatar, so a group spread along a cliff sounds like a group spread along a cliff.

The distance that counts as "nearby" is scaled to the scene, not fixed — derived from how far you are standing off what you are looking at, because outcrops span metres to kilometres and a room-sized constant would put everybody at a real outcrop on the silence floor. The leader is exempt: narration is always audible however far away they are.

Turn Spatial off and everyone is at full volume regardless of distance.

Live only

Voice is never recorded and never reaches the trip recorder — recording captures movement, not sound. Observers neither send nor receive audio, matching their role elsewhere.

Video — the camera tile​

A trip can also carry live video: one tile above the tabs showing the person narrating, so a guided trip has a face as well as a voice.

Camera is off by default and nobody is ever on camera without turning it on.

ControlWhat it does
🎥 CameraTurn your own camera on or off. It reads 📹 Camera On while it is running.
🫧 BackgroundChoose what appears behind you. Only offered while your own camera is on. See Backgrounds.
📹 Off (leader, on a participant's row)Cut that participant's camera.
📹 Allow (leader)Lift the block afterwards — the only thing that does. It restores the participant's own choice; a leader can never switch somebody's camera on for them.

The roster marks a camera-on participant with 📹 Camera, and a blocked one with 📹 Blocked.

What the tile shows​

The tile is pinned to the trip leader. Active-speaker switching was built and tried in a live session, and testers found it distracting: on a field trip the thing worth watching is whoever is narrating, not whoever last said "mm-hm".

So in practice only the leader's camera is broadcast. A participant can still turn their camera on — it stays a local preview, and goes live the moment leadership passes to them, which is routine in this application. Their self-view says so.

  • The tile is collapsible, and hidden entirely while nobody is publishing — a trip with no cameras looks exactly as it did before this feature existed.
  • A small self-view inset appears whenever your own camera is running, so you can always tell at a glance that you are being captured. When the tile is you, the self-view fills it.
  • When there is nothing coming, the tile says which of the two reasons it is — "Leader's camera is off" rather than "Waiting for video…" forever.
  • The tile follows a leadership transfer.
  • Joining late shows a picture straight away. Video can only be picked up from certain frames, so a latecomer used to wait for the next one; the server keeps a copy and hands it over on arrival instead.

Backgrounds​

🫧 Background decides what the trip sees behind you. Press it for a short menu:

ChoiceWhat you get
🚫 OffYour room, as the camera sees it.
🫧 BlurYour surroundings, blurred.
🟩 Green screenA flat chroma green (#00B140) behind you.
🟦 Blue screenThe same in chroma blue (#0047BB) — useful if you are wearing green.
⬛ Black / ⬜ WhiteA plain studio backdrop.

The button then reads back the choice that is running, and the menu ticks it.

VRGS gets there two ways, and picks for you:

  1. Your camera's own blur, where the hardware supports it (on Windows this is Windows Studio Effects, running on the NPU). Best quality, and switching it on does not interrupt anything. Camera hardware can only blur, though — so choosing a colour moves you to the second method.
  2. Software segmentation — VRGS separates you from the background and puts the blur or the colour behind you. Moving between the two methods restarts the publish, which costs viewers a moment of catch-up; changing colour, or turning the effect off, does not.

A colour background is the cheaper of the two effects, not the more expensive one: there is nothing to blur, so that work is skipped entirely. It also encodes far more efficiently than a real room, which means more of the tile's bitrate goes to your face — a flat background usually looks sharper, not just tidier.

What a green screen here is and is not for

The tile is a small, low-bitrate stream, so a viewer cannot cleanly key you out of it in other software — the green will be soft at the edges. Treat it as a privacy backdrop and a bitrate saving, not as a production chroma key.

Your choice lasts as long as the camera does; turning the camera off forgets it, and a restart is always Off. On a machine that can do neither method, VRGS says "Background effects are not available on this machine" once and stops offering the button. The camera itself is unaffected either way.

Privacy​

  • Opt-in and never remembered. Camera state is per-session and is never written to the project or to your settings — a restart is always camera-off.
  • Live only. Video is never recorded, stored, or sent to the trip recorder.
  • Force-off is enforced at the server, not just in the sending client's interface, and a block deliberately survives the blocked participant turning their own camera off.
  • Observers cannot publish, consistent with being send-deaf for audio.
  • A field-trip camera sees whoever is standing behind you. The always-visible self-view and the explicit opt-in are what guard against that; a background effect helps.
  • A picture you pick as a background is never uploaded. It is used on your own machine to draw your outgoing video, and is dropped when the camera stops.
Not in the 3D view or in VR

The tile lives in the VFT panel only. There is no video in the 3D scene and none in a VR headset, and there is no participant grid or screen sharing — one tile, one publisher.

The 🎥 Camera button is missing

It appears only when the whole path is possible: a live trip, a video-capable runtime, and a server that is hosting video. If any of those is absent the button is hidden rather than shown as something that would fail.

6. Record & replay​

The leader can start/stop recording at any time (or host with auto-record on). While recording, the server samples everyone's position over time. Recorded sessions are listed in the admin dashboard, where they can be reviewed for debrief along with the movement analytics (position trails and attention heatmap).

7. Leave or end​

  • A participant can leave at any time with 🚪 Exit Trip in the tool row; they are removed from the roster and everyone is notified.
  • The leader can end the trip, which closes it for everyone, stops any recording, and finalises the recording for replay.
  • If the leader simply disconnects (e.g. loses network), the server promotes the next member to leader so the trip keeps running; if nobody is left, the trip is ended and any recording is finalised automatically.
  • If the owner leaves, the trip continues for everyone still in it, but no new participants can be admitted until the owner returns (the panel shows an "owner away" note). When the owner signs back in they resume ownership automatically and can admit people again. See Ownership vs leadership.

The admin dashboard​

The dashboard is a web app for instructors and administrators. After signing in (and, in production, only for accounts granted admin access) it provides:

  • Dashboard — live counts and a Live Events feed (trips created/ended, participants admitted, recordings started/stopped, disconnects).
  • Trips — every trip (active, scheduled, ended) with its roster, chat, annotations, waypoints (including a GeoJSON export), and recordings. An admin can force-end a trip here.
  • Recordings — recorded sessions and their snapshots, for replay/debrief.
  • Users — everyone who has joined, by sign-in identity.
  • Analytics — per-trip attention heatmap (where participants looked) and position scatter (where they went), built from recorded movement.
  • Audit — a log of lifecycle and privileged actions (trip create/end, waiting room approve/reject, role changes, recording start/stop, admin force-end).

Running the server (for operators)​

The backend is a single self-contained binary/container (geotour-server). It needs PostgreSQL and, recommended, Valkey (Redis-compatible).

Quick start (local / development)​

A docker-compose.yml brings up the server, PostgreSQL, Valkey, and Prometheus:

docker compose up -d

The dev compose file sets ALLOW_INSECURE_DEV=true, so the server runs without authentication (any token is accepted and used as the display name) — for local use only. Point the VRGS client's VFT panel at ws://localhost:8080/ws.

Production​

Run the container (or binary) with the environment variables below. In production you must configure Auth0 — the server refuses to start without it unless ALLOW_INSECURE_DEV=true is explicitly set.

Configuration reference​

All configuration is via environment variables:

VariableDefaultPurpose
PORT / HOST8080 / 0.0.0.0Listen address.
DATABASE_URL(required)PostgreSQL connection string.
VALKEY_URL(empty)Valkey/Redis URL. Empty = no cache (presence, recording, analytics, and rate limiting are disabled).
AUTH0_DOMAIN / AUTH0_AUDIENCE(empty)Auth0 tenant + API audience for JWT validation. Required unless ALLOW_INSECURE_DEV=true.
ALLOW_INSECURE_DEVfalsePermit booting with no authentication (local development only).
ALLOW_GUEST_CONNECTIONStrueAllow unauthenticated guests to join trips. false requires every connection to be signed in. Hosting always requires sign-in regardless.
ADMIN_PERMISSION(empty)An Auth0 permission/scope that grants admin (dashboard + admin API) access.
ADMIN_EMAILS(empty)Comma-separated email allowlist for admin access (alternative to ADMIN_PERMISSION).
ALLOWED_ORIGINS(empty)Comma-separated allowed web origins for CORS and dashboard WebSocket. Empty = allow all (dev).
TRUST_PROXYfalseDerive the client IP from X-Forwarded-For/X-Real-IP. Enable only when behind a trusted reverse proxy / load balancer.
MAX_TRIPS100Maximum concurrent trips.
MAX_CLIENTS500Maximum concurrent client connections.
MAX_PARTICIPANTS_PER_TRIP50Default cap when a host doesn't set one.
WAITING_ROOM_TIMEOUT_SEC300How long a person waits before the queue times them out.
RECORDING_SAMPLE_HZ1Recording sample rate (positions/second).
RECORDING_FLUSH_SIZE60Snapshots buffered before a write.
HEARTBEAT_INTERVAL_MS5000Server→client heartbeat interval.
MAX_NAME_LENGTH / MAX_MESSAGE_LENGTH32 / 1024Name / chat length caps.
RATE_LIMIT_CONN_PER_MIN10Per-IP connection rate limit (needs Valkey).
RATE_LIMIT_MSG_PER_SEC50Per-client message rate limit (needs Valkey).
LOG_LEVELinfoinfo or debug.

Security model​

  • Authentication — clients present an Auth0 access token (/ws?token=…); the admin API uses Authorization: Bearer …. The server validates the token's signature, audience, issuer, and expiry.
  • Authorization — the admin dashboard/API additionally requires an admin identity: a token carrying ADMIN_PERMISSION, or an email in ADMIN_EMAILS. If neither is configured, every authenticated user is treated as admin (logged as a warning) — set one in production.
  • Fail-closed — without Auth0 configured the server refuses to boot unless ALLOW_INSECURE_DEV=true.
  • Origins — set ALLOWED_ORIGINS to your dashboard's URL in production.
  • Behind a proxy — set TRUST_PROXY=true so per-IP limits use the real client IP rather than the load balancer's.

Health & metrics​

EndpointPurpose
GET /healthzLiveness (process is up).
GET /readyzReadiness — checks PostgreSQL and Valkey connectivity.
GET /metricsPrometheus metrics (connections, trips, messages, heartbeat latency, auth failures, rate-limit hits, …).

What data is stored​

PostgreSQL retains: users (by sign-in identity), trips, participants, chat messages, annotations, waypoints, recordings and their position snapshots, movement trails, and the audit log. Valkey holds only transient live state (current positions, rate-limit counters). Operators should set retention/backup policies according to their privacy requirements, since recordings and trails capture participant movement.

Voice and video are never stored. They are relayed live and dropped; the trip recorder captures movement, not sound or pictures. Camera state is per-session and is never written to the project or to your settings.

Troubleshooting​

SymptomLikely cause / fix
VFT panel can't connectThe geotour-server isn't running or the URL is wrong. Start the server and check the host/port the panel targets.
"missing token" / "invalid token" on connectThe client isn't signed in, or the server's AUTH0_DOMAIN/AUTH0_AUDIENCE don't match the token. In dev, set ALLOW_INSECURE_DEV=true.
Stuck in the waiting roomNo leader/TA is admitting people, or the wait timed out (WAITING_ROOM_TIMEOUT_SEC). Re-join after the leader is ready.
"trip is full" when approvingThe trip reached its participant cap. Raise the cap when hosting, or remove someone.
"server at capacity" (503) on connectMAX_CLIENTS reached. Raise it or scale the server.
Can't chat / annotate / moveYou're an Observer, or a TA/leader needs to promote you (see the permissions matrix).
Scheduled trip didn't startThe server activates scheduled trips on a short timer once their start time passes; confirm the server clock and that the trip's start time has arrived.
Dashboard is empty or unauthorisedYour account lacks admin access — configure ADMIN_PERMISSION or ADMIN_EMAILS.
No analytics / recordingsValkey isn't configured (VALKEY_URL); presence-driven features require it.
Nobody can hear meCheck the 🎙 Mic button is not red (self-muted), that you are holding ` in push-to-talk mode, and that the leader has not muted everyone. If the voice buttons never appear at all, VRGS could not open an audio device.
My microphone is fine but a busy trip didn't carry meSix voices are carried at once, and several people were already talking — see How many people can talk at once. Wait for a gap, or ask the leader for 🔇 Mute All and hand-raise.
I can hear an echo of everyoneOpen speakers with no echo cancellation. Use headphones, or switch to push-to-talk.
My typing is going out over the microphoneIt should not — isolated keystrokes neither open nor hold the microphone open, and are ducted while push-to-talk is held. Persistent noise usually means a very close-coupled microphone; push-to-talk removes it entirely.
Distant participants are inaudibleThat is 🧭 Spatial doing its job. Turn it off for a flat mix; the leader is always exempt.
The 🎥 Camera button is missingIt needs a live trip, a video-capable runtime, and a server hosting video. Any one missing hides it rather than offering something that would fail.
My camera is on but nobody sees meOnly the leader's camera is broadcast. Yours stays a local preview until leadership passes to you.
The camera button is disabledThe leader has turned your camera off. Only they can lift the block (📹 Allow on your roster row).
The tile says "Leader's camera is off"Exactly that — nothing is being published. It is not a slow start.
🫧 Background disappearedThis machine can do neither hardware nor software background effects. VRGS says so once and stops offering it; the camera is unaffected.

See also​