Skip to content

Dataland Architecture

Welcome. This is the operator + contributor map of the Dataland backend stack: the set of microservices that powers the museum guide app, the live visitor experience, and the curator tooling behind Refik Anadol Studio's Dataland museum.

If you have just landed on the project, read Architecture for the network topology and data flows, then the Services reference for the per-service detail. If you came here because something broke, jump straight to Deploy and Observability.

What Dataland is

Dataland is a physical AI-art museum. Visitors wear biosensor bands; sensor data flows from the RDC (Refik Anadol data center) into this stack, where it drives a live AI guide and a context-aware notification engine. The backend has four jobs:

  1. Talk to visitors. An AI guide (agent) answers questions over SSE, grounded in live room/vitals telemetry and a retrieval corpus of artwork and museum knowledge.
  2. Ingest the live experience. The museum bridge reads the external RDC redis (the wearable/sensor source of truth) and normalizes it into an internal telemetry stream.
  3. React to the visit. The notification engine consumes that telemetry and fires OneSignal pushes (welcome, room-transition tips, session-flow nudges, checkout) plus internal ops alerts.
  4. Keep the corpus current. Curators use the Catalog Studio (the knowledge service, formerly information-webui) to manage the artwork + museum catalog, which live-syncs into the RAG vector store.

What runs where

The whole stack runs on a single Linux host: the Spark DGX VDS (tailnet IP 100.124.170.43, repos under /home/cobanov/DATALAND/), under Docker Compose. The dataland-infrastructure repo is the orchestrator: it owns the single compose.yml, the deploy script, and the source for this docs site.

One compose.yml brings up 10 services in two rings:

Layer Services
Data plane postgres, redis, qdrant
App services auth, agent, rag, museum-api, notification-worker, notification-api, knowledge

App-level observability is Logfire only; the self-hosted metrics stack was turned off (2026-07). GCP is only an API/storage consumer (the Gemini API + GCS buckets). This docs site is built by Cloudflare Pages from docs/src/ — it is not a container in the stack.

The compose.sim.yml overlay adds museum-simulator (and an isolated redis-sim/telemetry-sim pair) for synthetic telemetry. The simulator is not part of production. In production, museum-api itself bridges from the external RDC redis.

Naming

The compose name: is dataland, so containers are dataland-agent, dataland-auth, dataland-knowledge, dataland-postgres, and so on. Internal callers use those container DNS names over the Docker network (http://dataland-rag:4143), never host-published ports. See Architecture.

Service map at a glance

Service Container Role Public port Internal port
Agent dataland-agent AI guide. Museum + general chat over SSE, conversation history, service + ops endpoints 4141 (0.0.0.0) 4141
Museum API dataland-museum RDC redis bridge, telemetry normalizer, chapters catalog + dashboard 4144 (0.0.0.0) 5001
RAG dataland-rag Retrieval. Qdrant search + ingest, Gemini captioning + embeddings 4143 (loopback / tailnet) 4143
Knowledge dataland-knowledge Catalog Studio CMS. Projects + Museum workspaces, RAG live-sync 4152 (0.0.0.0) 4152
Notification worker dataland-notification-worker Telemetry rules engine → OneSignal pushes + ops alerts — (no HTTP)
Notification API dataland-notification-api DLQ + replay + state inspector ops surface 8080 (loopback / tailnet) 8080
Auth dataland-auth JWKS provider for RS256 JWT chat auth (runs from the agent image) 9000 (loopback / tailnet) 9000
Redis dataland-redis museum:telemetry stream, ticket state, dedup keys (requirepass) 4145 (loopback / tailnet) 6379
Postgres dataland-postgres Agent database (dataland) + the auth server's auth_server_users 5432 (loopback / tailnet) 5432
Qdrant dataland-qdrant Vector store: knowledge, images, scenes 4146/4147 (loopback / tailnet) 6333/6334

For the authoritative port + bind matrix, see Public ports.

The 30-second mental model

flowchart LR
  subgraph external["External"]
    Mobile["Mobile app<br/>(visitor)"]
    RDC[("RDC Redis<br/>wearable/sensor<br/>source of truth")]
    OneSignal["OneSignal"]
    GCS[("GCS<br/>public + private")]
    Gemini["Gemini API<br/>(gemini-3.5-flash<br/>+ gemini-embedding)"]
    Curator["Curator"]
  end

  subgraph dataland["Dataland host (Spark, 100.124.170.43)"]
    Auth["auth :9000"]
    Agent["agent :4141"]
    Museum["museum-api :5001"]
    RAG["rag :4143"]
    WebUI["knowledge :4152"]
    NotifW["notification-worker"]
    NotifA["notification-api :8080"]
    Redis[("redis :6379")]
    PG[("postgres :5432")]
    QD[("qdrant :6333")]
  end

  Mobile -->|SSE chat| Agent
  Agent -->|JWKS verify| Auth
  Auth --> PG
  Agent --> RAG
  Agent --> Museum
  Agent --> PG
  Agent --> Redis
  Agent -->|complaint detect| NotifA

  Museum -->|PSUBSCRIBE| RDC
  Museum -->|XADD museum:telemetry| Redis

  NotifW -->|XREADGROUP| Redis
  NotifW -->|service call| Agent
  NotifW --> OneSignal

  RAG --> QD
  RAG --> GCS
  RAG --> Gemini

  Curator --> WebUI
  WebUI --> RAG
  WebUI --> GCS

Two flows do most of the work:

  • Telemetry → reaction. museum-api PSUBSCRIBEs the external RDC redis, normalizes each event, and XADDs it to the internal museum:telemetry stream on dataland-redis. The notification worker XREADGROUPs that stream, evaluates rules, and fans out OneSignal pushes (and ops alerts). See the telemetry sequence.
  • Chat. The mobile app POSTs to the agent with a Bearer JWT; the agent verifies it against the auth JWKS, calls its tools (get_visitor_vitals, get_room_info, get_scene_flow, search_knowledge, search_artwork_images) against museum-api and RAG, and streams tokens back over SSE. See the chat sequence.

Edge + access

flowchart LR
  Internet(("Internet")) --> CF["Cloudflare Tunnel<br/>(host systemd, token mode)"]
  Tailnet(("Tailscale tailnet")) -.->|*_PUBLIC_BIND| Host
  CF --> Host["Spark host<br/>published ports"]
  Host --> Agent["agent → dataland.chat"]
  Host --> Museum["museum-api → museum dashboard"]
  Host --> WebUI["knowledge → knowledge.dataland.chat"]
  • Cloudflare is the only public ingress. cloudflared runs as a host systemd service and routes the public hostnames (dataland.chat, knowledge.dataland.chat, and the museum dashboard) to the right local port. There is no nginx/Traefik in front of the stack. TLS terminates at Cloudflare.
  • Tailnet is the second access path. Stateful + internal services publish on 127.0.0.1 and on a *_PUBLIC_BIND host IP (default 100.124.170.43, the tailnet interface) so tailnet peers reach them directly without an SSH tunnel. Never 0.0.0.0. See Public ports.
  • This docs site is built and served by Cloudflare Pages from docs/src/ — it is not a container in the stack. Pushing a change to docs/src/ triggers a rebuild.

How to navigate these docs

You want to… Go to
Understand the topology + data flows Architecture
Look up what a service does, its ports, env, and auth Services → the per-service page
Find which port / Cloudflare hostname maps where Public ports
Deploy, rebuild, or roll back Deploy
Debug something slow, or read traces / logs Observability
Find on-disk state, volumes & backups Host state & backups
Run schema / data / vector migrations Migrations

Quick service jumps: Agent · Museum · RAG · Knowledge · Notification · Auth · Redis · Postgres · Qdrant.

Models

The stack is standardized on Google Gemini:

Use Model Where
Chat (museum + general) gemini-3.5-flash agent (agent_model = google-gla:gemini-3.5-flash)
Image captioning at ingest gemini-3.5-flash rag (gemini_model)
RAG vector embeddings gemini-embedding-2 rag (embedding_model)

Single model

Everything chat- and caption-facing runs gemini-3.5-flash; the model id is read from .env (GEMINI_MODEL / AGENT_MODEL) at boot. RAG vector embeddings are a separate model (gemini-embedding) and are not changed by a chat-model bump.