Docs/Timebase Components/Timebase Atlas Architecture
AtlasConcept

Timebase Atlas Architecture

Timebase Atlas ships as one container running one process.

Updated 26 Oct 2026Timebase Docs

Overview#

Timebase Atlas ships as one container running one process. That single process serves the browser interface, the REST API, three SignalR hubs, an MCP endpoint and an i3X read surface, and it also is the runtime: it stores your model in Git, compiles it, holds the compiled result in memory, executes Cypher queries against it, receives live values from connectors and streams them out again.

Two external Timebase components attach to it over the network, and both connect outward to Atlas, never the reverse:

  • Connector brings data in.
  • Bridge takes data out.

One platform component is a prerequisite rather than an attachment: Pulse, the Timebase identity provider. Atlas ships with authentication on, and it is also the only route to the AI assistant's models.

This article describes what actually runs, which surfaces exist, where your model is stored, how a value travels from a data source to a downstream system, and the limits of the current design. It is written for someone planning, deploying or diagnosing an Atlas installation.

The one thing to take away: Atlas is a single-instance, single-process system today. There is no separate runtime service to install, no cache tier, no message bus, and no supported way to run two Atlas instances against one data volume.

The shape of a deployment#

Picture four boxes and three arrows.

In the middle is one box: the Atlas container, listening on 4551 (HTTP) and 4552 (HTTPS). Inside it, and not separable from it, are the browser UI, the API, the Git-backed store of your models, the compiler, the in-memory compiled worlds, the Cypher query engine and the live-value machinery.

Around it:

  • Connectors (one or more, on any host) hold an outbound SignalR connection into /hubs/connector and push rows of data up.
  • Bridge (optional, one or more) holds outbound SignalR connections into the value hub, one per (branch, domain) world, and writes what it receives to SQL, MQTT, Kafka, Event Hubs, Snowflake, HTTP and so on.
  • Pulse issues the bearer tokens that every Atlas surface checks, and proxies the AI assistant's model traffic.

There is no fifth box. Nothing sits between the connector and Atlas, and nothing sits between Atlas and Bridge.

Ports#

Component Port Notes
Atlas 4551 HTTP. HTTPS redirection is active, so plain HTTP is answered with a redirect to 4552
Atlas 4552 HTTPS. Exposed by the image. Publish it in your Compose file to reach Atlas over TLS
Pulse 4542 The shipped Pulse:Port default
Bridge 4571 / 4572 Bridge's own /status endpoint, not an Atlas surface

The ports come from Kestrel:Endpoints in settings.config, not from ASPNETCORE_URLS, which controls nothing in Atlas.

Important

publish 4552 alongside 4551, or terminate TLS in front of Atlas. The redirect from 4551 points at it, and browser features such as the dictation microphone in the Atlas interface need a secure origin.

What Atlas ships as#

Atlas deploys as one process, Atlas, with two supporting libraries that other Timebase components reference.

Piece What it is
Atlas The single .NET host: API, interface hosting, hubs, MCP, i3X, Git store, compiler, world cache, connectors and reactors
The Atlas interface The React single-page application. It is built during the Atlas publish and served from the same process, so it is not a separate deployable
Query engine The Cypher engine: grammar, analysis, planning, execution, pushdown and subscriptions. A library referenced by Atlas
Value hub client A strongly typed SignalR client for the value hub. Bridge and Explorer use it
Shared model and contracts The compiled-world types, the connector and value-hub wire contracts, and the Atlas vocabulary

What runs inside Atlas#

These are the subsystems of that one process.

Subsystem Responsibility
Git store The Git bootstrap, the per-repository lock, and the SHA-pinned read path
Persistence Reading and writing the JSON-LD ontology and the instance-map files
Sessions Draft sessions: per-identity private worktrees for saves with outstanding violations, plus publish and clean-up
Compilation Building the compiled world, compiling function bodies, the world cache, and the startup recompile
Connectors The in-memory connector registry, binding-path resolution, subscription orchestration and backfill dispatch
Data store Where an inbound row becomes a cell in the compiled world
Reactors Recomputation of calculated and reference properties, driven by the in-process property change bus
Views Saved Cypher queries, stored per domain
Agent The Atlas AI assistant: tool providers over domains, ontology, instance maps, Cypher and domain lifecycle
MCP Exposes a selected subset of those tools over the Model Context Protocol
Licensing Effective-licence projection and telemetry
Write path Worktree, commit, branch advance and draft routing, alongside the read path

Two more pieces sit behind the value hub: the live-value fan-out, and a background service that maps property-change events onto value-hub subscription keys.

The eight top-level surfaces#

Atlas maps eight top-level route families. The first is the general-purpose integration surface.

Surface Route Credential What it is
Machine plane /api/v1 Pulse bearer The public REST API. Use this for integrations
UI plane /app Session cookie, admin role for most writes The route set the Atlas web interface uses for itself, not published in the API documents. Build against the /api/v1 twin of the same resource
Connector hub /hubs/connector Cookie or bearer SignalR. Connectors handshake here and push rows
Value hub /hubs/v1/branches/{branch}/domains/{domain}/subscribe Cookie or bearer SignalR. Live values, pinned to one world by the URL. This is Bridge's and Explorer's entry point
Log hub /hubs/logs Cookie or bearer SignalR live log tail. Its history twin is GET /api/v1/logs
MCP /mcp Pulse bearer Model Context Protocol endpoint exposing whitelisted read and skill tools to external AI clients
i3X /i3x/v1 Pulse bearer, except GET /i3x/v1/info A standards-conformant interoperability read surface with its own {success, result} envelope, not problem+json. Branch and domain are pinned by the I3x config section and never appear on the wire
API documentation /api/help, /api/v1.json, /api/i3x.json Anonymous in code Swagger UI and the OpenAPI documents, registered unconditionally

The three hubs all ride MessagePack, not JSON. A browser client must add @microsoft/signalr-protocol-msgpack; the default JSON protocol cannot discriminate the Column union and you get untyped property bags.

Anything that matches none of these is answered by app.MapFallbackToFile("index.html"), so a wrong path returns the SPA shell as 200 text/html rather than 404.

How Atlas stores your model#

Atlas keeps your domains and libraries in a Git repository on the data volume. Branches are versions, every write is a commit, and the container therefore has a real git dependency: the image installs git and sets git config --global --add safe.directory '*' so it can operate on a mounted volume.

The read and write paths are asymmetric.

Reads are worktree-free and SHA-pinned. A read resolves the branch to a commit SHA once, lock-free, then reads file contents straight out of the Git object database at that commit. No branch is created, no worktree is checked out, and neither the per-repository lock nor the per-branch lock is taken. Any number of reads run in parallel, and they run alongside writers, because a pinned-SHA read is unaffected by the branch tip moving.

Writes take a worktree and a lock. A write creates a transient worktree, commits into it, advances the branch, resyncs the root working tree, and removes the worktree. Writes to the same branch are serialised per branch, and every write additionally takes a per-repository lock. Because one server serves one repository, that lock serialises the write path across the process.

Two boot-time behaviours are worth knowing about:

  • Garbage collection is pinned off. Every boot sets gc.auto=0 and core.autocrlf=false, so that background repacking cannot run under a live read. Loose objects accumulate, so plan for steady growth on the data volume and pack the repository against a stopped instance.
  • The system library is kept current. Atlas compares its own copy of atlas-core against yours at every start, copies it in if it is missing, and restores the shipped version if they differ. This is how a framework vocabulary upgrade reaches your model with a new image.
Important

put your own types in a library of your own that imports atlas-common. Your library is never rewritten, and neither are the libraries you copied in during commissioning, atlas-common included.

Inside the Config directory but outside Git history are compiled-worlds.json, draft-sessions.json, /agents/, /skills/, /views/ and /.worktrees/; the assistant's chat state lives in the Data directory. A clone or bundle of the repository therefore leaves behind your agent and skill definitions, saved views, the compiled-world records that drive restart recompilation, and every author's unpublished draft session, and does not carry the Data directory. Back up the whole Atlas directory or volume.

Draft sessions also cost disk: each one is a real checked-out worktree at {root}/.worktrees/.v2-draft/{hash}, one per (identity, target branch).

Compiling, and the World#

A domain is authored, then compiled. Compiling is PUT /api/v1/branches/{branch}/domains/{domain}/world. It validates the whole domain before it builds, so while any error-severity violation is outstanding it returns 422 with a violations[] array, and the World is built once the last one is cleared.

On success it produces a World: every type, every expanded instance, every relationship, in memory. Worlds are cached per branch and domain. Each entry holds the World itself, a query engine with its own persistent query cache, the compiled function registry, and the compiled functions for that model.

Three consequences that matter operationally:

  • Recompiling replaces the cached World in one step. Queries in flight finish against the previous World, and the new one answers everything after the swap.
  • A cached World stays resident for the life of the process. An entry is replaced by a newer compile of the same branch and domain.
  • Memory scales with branches multiplied by domains, not with domains. Because a branch is a version in Atlas, compiling the same domain on ten branches holds ten complete Worlds at once. This is the figure to size against.

Restart behaviour#

Atlas recompiles at boot. A background service loads every recorded (branch, domain) pair from compiled-worlds.json and recompiles them sequentially, one at a time, while Atlas is already bound and serving traffic. Domains become queryable one by one as each recompile lands.

So restart time to full availability grows roughly linearly with the number of compiled (branch, domain) pairs, and a query against a domain whose turn has not come yet reports the domain as not compiled. A handful of domains clear in seconds; a large estate takes longer. No manual recompile is needed either way.

One class of domain compiles later: a domain whose instances come from a connector-driven source has its declared binding paths captured at boot, and it is compiled the moment a connector advertises paths that resolve them.

The knob is Compilation:AutoRecompileOnStartup, default true.

The runtime data path#

Ingress: from a data source to the compiled World#

The whole path is in one process. There is no hop out of it at any point.

  1. A connector connects outward to /hubs/connector and calls Handshake(ConnectorAdvertisement), advertising the concrete binding paths it can serve. Atlas answers with a HandshakeResponse carrying the AtlasSessionId, so the connector can detect an Atlas restart.
  2. Atlas resolves the binding paths declared in your instance maps against the in-memory connector registry and asks the connector to Subscribe to the ones it needs. GetRange, Subscribe and Unsubscribe are handlers on the connector that Atlas invokes.
  3. The connector pushes unprompted as data arrives: PushSubscriptionRows(subscriptionId, CompoundRowSet, EmitKind, asOfT), plus PushSubscriptionStatus and PushSubscriptionWatermark.
  4. Atlas writes the rows into the compiled World's columnar store and publishes a change on the in-process property change bus.
  5. The calculated-property and reference-property reactors pick that change off the bus and recompute dependent properties.
  6. The change is mapped onto value-hub subscription keys and streamed to whoever is subscribed.
Note

connectors push live values and Atlas asks them for ranges of history, so there is no single-value poll. A bindingPath is slash-delimited, with the connector instance id as its first segment, and carries no scheme:// prefix.

Important

a binding path is matched against the connector's advertised paths at start-up, so copy the path rather than typing it. A bound property that has not received its first sample holds no value, so it reads as null.

Egress: Bridge#

Bridge is the counterpart to Connector, and its connection is one hop, not three.

Bridge opens a SignalR connection directly to the value hub at /hubs/v1/branches/{branch}/domains/{domain}/subscribe, one connection per world, using the shared value hub client library. There is no routing layer between them and nothing in Atlas dispatches to Bridge.

Each Subscribe… method on the hub is a server-to-client stream of ValueRowSetDto over MessagePack. Cancelling the stream is the unsubscribe; there is no separate call. Streams do not auto-restore after a reconnect, so a client must re-establish them. A fifth method, SubscribeToTypeDescription, streams a type's compiled shape (its columns, endpoints or enum roster) rather than values. See Atlas API - Subscribing to Live Values.

A Bridge value subscription declares one of exactly four shapes:

SubscriptionMode What it delivers
InstanceProperty One property of one instance. Single column, no per-row URIs
Instance Every property of one instance. Multi-column, no per-row URIs
TypeProperty One property across every instance of a type. Single column, per-row URIs
Type Every property of every instance of a type. Multi-column, per-row URIs

A Bridge Type description subscription (TypeDescription) is a fifth kind that follows SubscribeToTypeDescription instead of a value stream. It is chosen when the subscription is added and carries a type and no instance. See Exporting Data with Bridge.

Transformation and delivery are the job of a per-destination plugin over a shared rowset decoder: Microsoft.SqlServer, PGDG.PostgreSql, Oracle.MySql, IETF.Http, MQTT, Apache.Kafka, Microsoft.EventHubs and Snowflake.SnowpipeStreaming. A single Bridge instance can feed several sinks at once.

Queries#

A query is parsed and executed in process, against the cached compiled World for that branch and domain. Nothing is routed elsewhere, so there is no query placement to tune.

Querying a domain that has never been compiled returns 400 with "code": "Query.NotCompiled".

Point normalization#

When several inputs arrive at different times and rates, Atlas can align them onto a common instant before a calculation runs. This is a host-level setting, not a per-domain one: LiveProjection:AllowedLatencyMs, default 2000. It also requires opting in on the data type (a time-axis field) and on the function (contributing inputs). It applies to calculations and to live full-instance and full-type subscriptions.

Pulse is a prerequisite, in three separate ways#

The shipped settings.config contains:

"Pulse": {
  "Host": "localhost",
  "Port": 4542,
  "UseTls": true,
  "Auth": {
    "Enabled": true,
    "ClientId": "Atlas",
    "ClientSecret": ""
  }
}
  1. Authentication is on by default. Every /api/v1 route, all three hubs, /mcp and /i3x/v1 require a credential. A caller's Pulse token must list Atlas (the value of Pulse:Auth:ClientId) as an audience.
  2. The browser interface redirects to Pulse. The page shell loads anonymously, resolves the signed-in identity, and redirects to the sign-in flow when there is none.
  3. The AI assistant reaches its model through Pulse. Agent models are Pulse-proxied Gage, discovered from Pulse's own catalogue and configured as Pulse:Gage {Enabled, Agents, Guardrails, Dictation, Resilience}. ANTHROPIC_API_KEY is forwarded by docker-compose.yml and is not read by Atlas.

You can set Pulse:Auth:Enabled to false for a local evaluation. Writes are then attributed to Anonymous, so use it to look around rather than as a deployment posture.

Licensing is separate. /app/license projects the verified effective licence to the interface for display.

How Atlas scales today#

Important: Atlas runs as a single instance. Run one Atlas process per data volume and scale it by giving that machine more memory and CPU.

Six pieces of state are held per process, which is what makes this the supported shape:

  • Hub subscriptions are per process. SignalR runs without a backplane, so a subscription held on one instance is not visible to another.
  • The connector registry is per process. It is held in memory and keyed by live connection, so connectors attached to one process are known only to that process.
  • The world cache is per process. Compiled Worlds live in the same process that compiled them. Compiling on instance A leaves instance B still reporting the domain as uncompiled.
  • The Atlas session id is per process. One identifier is minted per process and stamped on every handshake, so a connector can detect an Atlas restart.
  • The write locks are process-local. The per-branch write lock and the per-repository Git lock are both held in the process, which is what serialises writes to one repository.
  • One server, one repository. The configured data root is authoritative, and one server serves exactly one repository.

What is true about growth:

  • Reads scale with CPU and do not contend, because the read path is lock-free and SHA-pinned.
  • Writes to the same branch are strictly serial, and all Git-mutating work in the process serializes behind the single repository lock. Authors on different branches avoid the branch lock but still meet the repository lock.
  • Memory is dominated by the cached Worlds, one per branch and domain, resident for the life of the process.
  • Splitting a very large model means splitting it into separate domains. A single domain compiles into one in-memory World and cannot be divided.

Sizing#

Four things drive Atlas's memory footprint:

  • the number of (branch, domain) pairs compiled at once,
  • the entity and edge count of each World,
  • the compiled function assemblies each World holds,
  • the number of live value-hub subscriptions.

For starting figures and the planning rule, see Hardware Requirements Guide.

Deployment topology#

The shipped docker-compose.yml declares exactly one service:

services:
  atlas:
    image: timebase/atlas:${TIMEBASE_TAG:-latest}
    hostname: atlas
    ports:
      - "4551:4551"
      - "4552:4552"
    volumes:
      - atlas:/atlas
    environment:
      - Certificates=/atlas/certificates
      - Settings=/atlas/settings
      - Config=/atlas/config
      - Data=/atlas/data
      - Logs=/atlas/logs
    restart: unless-stopped

One service, one volume. In the platform Compose file Atlas, Pulse, Connect and Bridge share one project and one network, so each reaches the others by service name - pulse, atlas - and nothing has to be started in a particular order beyond what the file's depends_on entries express.

Configuration precedence#

The configuration sources are layered in this order:

appsettings.json -> appsettings.{Environment}.json -> settings.config -> environment variables -> command line.

Because settings.config is added before the environment-variable source, the environment and the command line take precedence over the file for every key - Pulse:*, Kestrel:* and Serilog:* included. A value in the file holds only where nothing outranks it.

settings.Docker.config is not a file in the volume and not a layer that is loaded. It is an overlay embedded in the image, applied once: the first time Atlas starts into an empty volume it composes settings.config from its template patched with that overlay, which sets Pulse:Host to pulse, and never reads it again. Change the address in settings.config, or set Pulse__Host as an environment variable, which outranks the file.

Atlas reaches its language model through Pulse Gage, so no API key is configured on Atlas itself.

Troubleshooting#

Symptom Likely cause What to do
The browser interface will not open Nothing is listening, or you are on the wrong port Atlas serves the UI and the API from the same address. Open the root of https://<host>:4552 or http://<host>:4551. Confirm the container is up with docker compose ps and check docker compose logs. There is no separate UI port
A redirect sends you to a port that does not answer HTTPS redirection points 4551 at 4552, and the stock compose file publishes only 4551 Publish 4552 as well, or terminate TLS in front of Atlas
The container will not start, and the log mentions a native probe The read path depends on the native Git library, so Atlas stops at startup rather than coming up unable to serve reads Read the probe message in the startup log. Confirm the data volume is mounted and reachable, and that the image is intact
Every API call returns 401, or a browser sign-in page appears where you expected JSON Pulse:Auth:Enabled is true (the default) and either no bearer was sent or Pulse is unreachable Confirm Pulse is running and reachable on 4542. Send Authorization: Bearer <token> with Atlas in the token's aud. Disable redirect-following in API clients so you see the challenge rather than an HTML page
A request returns 200 text/html Atlas serves its single-page application for any URL outside the API surface Check Content-Type: application/json before parsing, then check the route. The current families are /api/v1/branches, /api/v1/worlds, and entity-types and relationship-types under a domain
A connector is running but does not appear in the Connectors panel It cannot reach /hubs/connector, or it cannot clear the hub gate Check the connector's Atlas address. In Docker use the service name atlas, not localhost. When auth is on, the connector's Pulse client registration must list Atlas's Pulse:Auth:ClientId as an audience, or its bearer will not validate
A domain compiles but shows no live data The binding path in the instance map does not match a path the connector advertises Compare the exact bindingPath against the paths shown on the Connectors panel. The first segment is the connector instance id and the separator is /, with no scheme:// prefix
A property shows a dash, or its value, timestamp and quality read null A bound property holds no value before its first sample Read an all-null row as "not receiving yet". Check that the tag is online, the connector is up, and the path matches
After a restart, a domain reports as not compiled Either the sequential boot recompile has not reached it yet, or it is connector-dependent and its connector has not advertised Wait and retry: domains become queryable one at a time. If it never becomes ready, check that its source connector is connected and advertising the paths its instance maps declare. You do not need to recompile manually after a restart
You saved and compiled, and nothing changed The save carried outstanding violations, so it is held in your private draft session, and a compile from a draft builds your own copy Check the Atlas-Validation-Violations response header, open the domain on the Overview screen, clear the Outstanding items panel, then publish the draft from the Unpublished changes panel
A compile returns 422 and no world exists Atlas validates the whole domain before it builds Read the violations[] array. Rule IDs starting O- are ontology items and I- are instance-map items. Clear O-A1 and O-A2 first if present, because the rest of the list follows from them
An edit to atlas-core was restored after a restart That library is system-owned and is compared against the installation on every start Put your changes in a library of your own
A calculation combines inputs from different moments Inputs arriving at different rates are not aligned unless you ask for it Point normalization is opt-in. Set the time-axis field on the data type and mark the contributing inputs on the function. The host-level setting is LiveProjection:AllowedLatencyMs
Memory climbs as the team works Compiled worlds are held per branch and domain for the life of the process Count the (branch, domain) pairs compiled on that instance rather than the domains, and tidy the branch list. See "How Atlas scales today"
Disk on the data volume keeps growing gc.auto=0 is pinned at every boot, so loose Git objects accumulate, and every active draft session holds a full worktree Plan for steady growth, and pack the repository with git gc against a stopped instance rather than enabling automatic garbage collection
A restore from a Git clone came back incomplete Agents, skills, compiled-world records and draft sessions live outside Git history Back up the whole data directory or volume rather than the repository
Article Owns
Timebase Atlas Key Concepts The vocabulary: domain, library, branch, World, compile, strategy, binding path
Atlas API Overview The /api/v1 surface, authentication, error format and pagination
Start Here for Docker: How to Deploy and Secure Timebase 2.0 Installing and first-run setup
Connecting Data Sources Setting up connectors and discovering the binding paths they advertise
Connector Diagnostics - Data Not Flowing Working through data that is configured but not arriving
Compiling and Publishing a Domain The compile lifecycle, staleness and draft publishing in operational detail
How Atlas Aligns and Times Values Point normalization, watermarks and what "current" means
esc
↑↓ navigate · ↵ open · ⌘K anywhere