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:
| Component | What it is | Who uses it |
|---|---|---|
| VRGS client | The desktop / VR application. The VFT panel (a dockable side panel) is the control surface for hosting and joining. | Instructors, students, trainees |
| geotour-server | A 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 dashboard | A 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.
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.
- 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.
| Capability | Observer | Participant | Presenter | TA | Leader |
|---|---|---|---|---|---|
| See the shared scene | ✓ | ✓ | ✓ | ✓ | ✓ |
| Broadcast their own avatar / movement | — | ✓ | ✓ | ✓ | ✓ |
| Chat & share resources | — | ✓ | ✓ | ✓ | ✓ |
| 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 | — | — | — | — | ✓ |
| 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.
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.
"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.
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).
- 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.
| Button | Who sees it | What it does |
|---|---|---|
| ✋ Hand | everyone | Raise or lower your hand. A Hand tag appears by your name for the whole trip. |
| 👀 Look Here | leader / owner | Arms a pick — your next click in the 3D view marks the spot for everyone. |
| ✏️ Ink | everyone | Arms the pen for freehand ink. Click again to put it away. |
| 🧽 Clear Ink | everyone, only when there is ink | Clears the ink (see Freehand ink). |
| 🔦 Pointers | everyone | Off: only the leader's pointer is drawn. On: everyone's, each labelled with its owner. |
| 📢 Recall All | leader | Brings every participant to your viewpoint. |
| 🎥 Follow | everyone except the leader | Your camera tracks the leader's as they move. |
| ⏸ Pause / ▶ Resume | leader | Pauses or resumes the trip. |
| 🎙 Mic, 🎚 Open Mic / Push-to-Talk, 🔇 Mute All, 🔊 Unmute, 🧭 Spatial | voice buttons appear once the audio engine is running; Mute All / Unmute are leader-only | See voice chat. |
| 🎨 Display | everyone except the leader | Follow the leader's shading and colouring, or keep your own. |
| 🚪 Exit Trip | everyone | Leave the trip (you are asked to confirm). |
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
- 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.)
- Give it a name, and optionally a password and a participant limit.
- Optionally enable auto-record so the session is captured from the start.
- 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.
- Create it — you become the trip's owner and its first Leader, and the panel shows an invite code with a Copy button.
- 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
-
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.
-
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.
-
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.
-
Roles — the leader can promote/demote members (e.g. make a co-instructor a TA, or set a guest to Observer).
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:
| Variable | Default | Purpose |
|---|---|---|
PORT / HOST | 8080 / 0.0.0.0 | Listen 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_DEV | false | Permit booting with no authentication (local development only). |
ALLOW_GUEST_CONNECTIONS | true | Allow 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_PROXY | false | Derive the client IP from X-Forwarded-For/X-Real-IP. Enable only when behind a trusted reverse proxy / load balancer. |
MAX_TRIPS | 100 | Maximum concurrent trips. |
MAX_CLIENTS | 500 | Maximum concurrent client connections. |
MAX_PARTICIPANTS_PER_TRIP | 50 | Default cap when a host doesn't set one. |
WAITING_ROOM_TIMEOUT_SEC | 300 | How long a person waits before the queue times them out. |
RECORDING_SAMPLE_HZ | 1 | Recording sample rate (positions/second). |
RECORDING_FLUSH_SIZE | 60 | Snapshots buffered before a write. |
HEARTBEAT_INTERVAL_MS | 5000 | Server→client heartbeat interval. |
MAX_NAME_LENGTH / MAX_MESSAGE_LENGTH | 32 / 1024 | Name / chat length caps. |
RATE_LIMIT_CONN_PER_MIN | 10 | Per-IP connection rate limit (needs Valkey). |
RATE_LIMIT_MSG_PER_SEC | 50 | Per-client message rate limit (needs Valkey). |
LOG_LEVEL | info | info or debug. |
Security model
- Authentication — clients present an Auth0 access token (
/ws?token=…); the admin API usesAuthorization: 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 inADMIN_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_ORIGINSto your dashboard's URL in production. - Behind a proxy — set
TRUST_PROXY=trueso per-IP limits use the real client IP rather than the load balancer's.
Health & metrics
| Endpoint | Purpose |
|---|---|
GET /healthz | Liveness (process is up). |
GET /readyz | Readiness — checks PostgreSQL and Valkey connectivity. |
GET /metrics | Prometheus 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.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| VFT panel can't connect | The 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 connect | The 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 room | No 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 approving | The trip reached its participant cap. Raise the cap when hosting, or remove someone. |
| "server at capacity" (503) on connect | MAX_CLIENTS reached. Raise it or scale the server. |
| Can't chat / annotate / move | You're an Observer, or a TA/leader needs to promote you (see the permissions matrix). |
| Scheduled trip didn't start | The 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 unauthorised | Your account lacks admin access — configure ADMIN_PERMISSION or ADMIN_EMAILS. |
| No analytics / recordings | Valkey isn't configured (VALKEY_URL); presence-driven features require it. |