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.
- Roles — every member 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; waypoints are ordered stops (with optional dip/azimuth) that define a route through the scene.
- Chat & resource sharing — text messages and shared links/resources, either to the whole trip or as a direct message.
- 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.
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.
2. Host a trip
- In the VFT panel choose Create / Host a trip.
- Give it a name, and optionally a password and a participant limit.
- Optionally enable auto-record so the session is captured from the start.
- Create it — you become the Leader, and the panel shows an invite code.
- Share the invite code with your group.
You can also schedule a trip for a future start time instead of starting it immediately; it appears in the trip list and becomes joinable when its start time arrives. (Scheduled trips persist on the server, so they survive a server restart.)
3. Join a trip
- In the VFT panel choose Join, enter the invite code (and password, if the trip has one), and your display name.
- You enter the trip's waiting room and see your queue position.
- When the leader or a TA approves you, you join the live trip as a Participant. 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, and a leader or TA can recall a single participant. 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.
- Annotations — pin notes in 3D. Participants and above can add them; presenters, TAs, and the leader can remove them.
- Waypoints — presenters/TAs/leader can place ordered stops (with optional dip & azimuth) that define a route through the scene.
- Chat & resource sharing — send messages to the whole trip or directly to one
member, and share resource links. (Messages are length-limited and links are
restricted to
http/https.) - 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; 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.
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). |
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. |