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:
- 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.
- 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.
- 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.
- Keep the corpus current. Curators use the Catalog Studio (the
knowledgeservice, 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-apiPSUBSCRIBEs the external RDC redis, normalizes each event, andXADDs it to the internalmuseum:telemetrystream ondataland-redis. The notification workerXREADGROUPs 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.
cloudflaredruns 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.1and on a*_PUBLIC_BINDhost IP (default100.124.170.43, the tailnet interface) so tailnet peers reach them directly without an SSH tunnel. Never0.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 todocs/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.