Skip to main content

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.
  • 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.

CapabilityObserverParticipantPresenterTALeader
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.
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.

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

  1. In the VFT panel choose Create / Host a trip.
  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. Create it — you become the Leader, and the panel shows an invite code.
  5. 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

  1. In the VFT panel choose Join, enter the invite code (and password, if the trip has one), and your display name.
  2. You enter the trip's waiting room and see your queue position.
  3. 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:

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).
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.

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.