---
sidebar_position: 5
title: "Services and adapters"
slug: /architecture/services-and-adapters
slide: /diagrams/abi-ports-adapters.svg
slide_alt: Primary adapters on the left (REST API, MCP server, CLI, NATS) call the engine services in the middle. Each service is a port, with its shipped secondary adapters on the right. The abi dev default of each port is outlined.
question: How do ABI's services, ports and adapters fit together?

---

# Services and adapters

The ABI engine is built as ports and adapters. Code never talks to a database, a store or a broker directly: it calls an engine **service**, and the service calls whichever backend `config.yaml` names. Swap the adapter, not the code.

Every table on this page is generated from the ABI commit pinned in `.abi`, so the list of adapters is the list the engine accepts.

## Ports and adapters in ABI

A call crosses three layers:

- **Primary adapters bring calls in.** The REST API (FastAPI, port 9879) mounts agents, workflows, pipelines and tools as endpoints. The MCP server exposes the API's agents as MCP tools. The `abi` CLI runs agents from a terminal (`abi chat`). In NATS mode, each kernel service also gets a NATS primary adapter that answers requests on `abi.svc.<service>.v1.*`, which is how remote modules and other engines reach it.
- **Each service is a port.** A service (`TripleStoreService`, `DocumentService`, `ObjectStorageService` and the others) defines what callers may do: insert triples, put an object, publish an event. Agents, workflows, pipelines and modules depend on the service, never on a backend.
- **Secondary adapters are the backends.** Each service has one adapter (the cache and secret services take a list), chosen under `services:` in `config.yaml`. The adapter turns the service's calls into SPARQL over HTTP, SQL, S3 requests or files on disk.

The engine reads `config.yaml`, validates every adapter block against its configuration model, then loads the adapters and wires the services together. A block that does not validate stops the engine before it serves anything.

```yaml title="config.yaml"
services:
  document:
    document_adapter:
      adapter: "postgresql"   # the secondary adapter
      config:                 # that adapter's settings
        dsn: "postgresql://{{ secret.POSTGRES_USER }}:{{ secret.POSTGRES_PASSWORD }}@postgres:5432/{{ secret.POSTGRES_DB }}"
```

## Services at a glance

| Service | `services:` key | Shipped adapters | Depends on |
|---|---|---|---|
| [Triple store](/architecture/services-and-adapters#triple-store) | `triple_store` | `oxigraph`, `apache_jena_tdb2`, `aws_neptune_sshtunnel`, `aws_neptune`, `fs`, `oxigraph_embedded`, `object_storage` | Bus, Object storage (nested), Key-value (nested) |
| [Vector store](/architecture/services-and-adapters#vector-store) | `vector_store` | `qdrant`, `qdrant_in_memory`, `sqlite_vec` | — |
| [Document](/architecture/services-and-adapters#document) | `document` | `sqlite`, `postgresql` | — |
| [Event](/architecture/services-and-adapters#event) | `event` | `sqlite`, `postgresql` | Bus, Dataset |
| [Activity log](/architecture/services-and-adapters#activity-log) | `activity_log` | `sqlite`, `document` | Document |
| [Dataset](/architecture/services-and-adapters#dataset) | `dataset` | `ducklake` | — |
| [Object storage](/architecture/services-and-adapters#object-storage) | `object_storage` | `fs`, `s3`, `naas`, `r2` | Event |
| [Key-value](/architecture/services-and-adapters#key-value) | `kv` | `redis`, `python` | — |
| [Cache](/architecture/services-and-adapters#cache) | `cache` | `fs`, `redis`, `object_storage`, `keyvalue` | Object storage, Key-value |
| [Bus](/architecture/services-and-adapters#bus) | `bus` | `rabbitmq`, `python_queue`, `nats_jetstream` | — |
| [Secret](/architecture/services-and-adapters#secret) | `secret` | `dotenv`, `naas`, `base64` | — |
| [Email](/architecture/services-and-adapters#email) | `email` | `smtp`, `filesystem`, `ses`, `sendgrid`, `microsoft_outlook` | — |
| [Source control](/architecture/services-and-adapters#source-control) | `source_control` | `forgejo`, `in_memory`, `local_git` | — |
| [Coding environment](/architecture/services-and-adapters#coding-environment) | `coding_environment` | `coder`, `code_server`, `in_memory`, `local_directory` | — |

Some parts have no adapter choice:

| Part | Config | Role |
|---|---|---|
| Model registry | `services.model_registry` | In-memory registry of chat and embedding models, fed by the provider modules that are enabled. Its only settings are the defaults. |
| Discovery | `nats.discovery` | The module registry one engine per project hosts on JetStream; remote modules register and renew a lease. |
| NATS | `nats` | The broker block. Present: kernel services, the bus (JetStream), module jobs and discovery go through NATS. Absent: everything stays in-process. |

### How services depend on each other

Loading a service loads what it needs. The triple store and the event service publish on the bus, and object storage records an `ObjectPut` or `ObjectDeleted` event for every write, so requesting object storage brings in the event service and the bus. Some adapters build on another service: the activity log's `document` adapter writes through the Document service, and the cache's `object_storage` and `keyvalue` tiers write through those services. The engine hands those services to the adapter at startup.

A few adapters carry a block of their own instead. The triple store's `object_storage` adapter builds its own object storage from the `object_storage_service` block under its `config`: pointing `services.object_storage` at S3 does not move the RDF files. Each service section below lists its dependencies and says which kind each one is.

## Presets

`abi new project` writes three configuration files. The engine reads `config.{ENV}.yaml` when the `ENV` variable names one that exists, and `config.yaml` otherwise. `ENV` comes from the environment, or else from the file of the first `dotenv` secret adapter in `config.yaml` (`.env` by default). Docker Compose sets `ENV=local`.

What each preset runs, service by service. "Engine default" means the template leaves the service out and the engine's default applies. Projects are created in NATS mode unless you pass `--without-nats`; the bus and the activity log then differ, as shown.

| Service | abi dev (`config.yaml`) | Docker Compose (`config.local.yaml`) | Remote (`config.remote.yaml`) |
|---|---|---|---|
| Triple store | `oxigraph` | `apache_jena_tdb2` | `apache_jena_tdb2` |
| Vector store | `sqlite_vec` | `qdrant` | `qdrant` |
| Document | `sqlite` | `postgresql` | `postgresql` |
| Event | `sqlite` (engine default) | `sqlite` (engine default) | `sqlite` (engine default) |
| Activity log | `document` (engine default); without NATS: `sqlite` (engine default) | `document` (engine default); without NATS: `sqlite` (engine default) | `document` (engine default); without NATS: `sqlite` (engine default) |
| Dataset | `ducklake` | `ducklake` | `ducklake` (engine default) |
| Object storage | `fs` | `s3` | `s3` |
| Key-value | `python` | `redis` | `redis` |
| Cache | `redis` (hot), `object_storage` (cold) (engine default) | `redis` (hot), `object_storage` (cold) (engine default) | `redis` (hot), `object_storage` (cold) (engine default) |
| Bus | `nats_jetstream` (NATS mode, no block); without NATS: `python_queue` | `nats_jetstream` (NATS mode, no block); without NATS: `rabbitmq` | `nats_jetstream` (NATS mode, no block); without NATS: `rabbitmq` |
| Secret | `dotenv` | `dotenv` | `dotenv` (the template's block does not validate) |
| Email | `smtp` (engine default) | `filesystem` | `smtp` (engine default) |
| Source control | `in_memory` (engine default) | `in_memory` (engine default) | `in_memory` (engine default) |
| Coding environment | `in_memory` (engine default) | `in_memory` (engine default) | `in_memory` (engine default) |

To change a preset one service at a time and get the diagram and `services:` block that go with it, use the [Architecture builder](/architecture/builder).

:::warning `config.remote.yaml` secret block
The pinned template gives the `dotenv` secret adapter a list (`config: [{path: .env.remote}]`) where the engine expects a mapping, so the engine rejects that file as written. Use `config: {path: ".env.remote"}` instead.
:::

## Services

Each section gives the service's role, its key in `config.yaml`, the adapters it ships, where each keeps its data, which preset uses it, and what it depends on. "Keeps its data" is the engine's own answer: only an engine whose services all keep their data in a backend every engine reaches can hand over to a new deploy without downtime (see [`nats_rpc`](#another-engines-service-nats_rpc)).

### Triple store

Stores ontologies and the knowledge graph as RDF triples and answers SPARQL queries. Config: `services.triple_store.triple_store_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `oxigraph` | Oxigraph server (HTTP) | A backend every engine reaches | abi dev | `requests` |
| `apache_jena_tdb2` | Apache Jena Fuseki (TDB2) | A backend every engine reaches | Docker Compose; Remote | `requests` |
| `aws_neptune_sshtunnel` | Amazon Neptune through an SSH bastion | A backend every engine reaches | — | `boto3`, `paramiko`, `requests`, `sparqlwrapper`, `sshtunnel` |
| `aws_neptune` | Amazon Neptune | A backend every engine reaches | — | `boto3`, `requests`, `sparqlwrapper` |
| `fs` | RDF files on local disk | On this host (files under storage/triplestore) | — | — |
| `oxigraph_embedded` | Oxigraph embedded in the engine process | On this host (embedded Oxigraph at storage/triplestore/oxigraph) | — | `pyoxigraph` |
| `object_storage` | RDF files in an object storage service | On this host (object storage: files under storage/datastore) | — | — |

Also accepts `nats_rpc` and `custom`.

Depends on:

- [Bus](/architecture/services-and-adapters#bus) (every adapter): broadcasts every inserted or deleted triple on the engine's bus, wired at startup.
- [Object storage](/architecture/services-and-adapters#object-storage) (`object_storage` only): its own object_storage_service block under config, not the engine's Object storage service.
- [Key-value](/architecture/services-and-adapters#key-value) (`apache_jena_tdb2` only): optional key_value_service block under config: a write lock shared across processes.

### Vector store

Stores embeddings in collections and answers similarity searches. Config: `services.vector_store.vector_store_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `qdrant` | Qdrant server | A backend every engine reaches | Docker Compose; Remote | `qdrant-client` |
| `qdrant_in_memory` | Qdrant embedded in the engine process | On this host (embedded Qdrant in this process) | — | `qdrant-client` |
| `sqlite_vec` | SQLite with the sqlite-vec extension | On this host (SQLite at storage/vectorstore/vectors.sqlite3) | abi dev | `sqlite-vec`, `sqlite3` |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Document

Stores documents in namespace-bound collections, one document per atomic write. Config: `services.document.document_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `sqlite` | SQLite | On this host (SQLite at storage/documents.sqlite) | abi dev | `sqlite3` |
| `postgresql` | PostgreSQL | A backend every engine reaches | Docker Compose; Remote | `psycopg` |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Event

Keeps the durable event log and broadcasts each published event on the bus. Config: `services.event.event_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `sqlite` | SQLite | On this host (SQLite at storage/events/events.sqlite) | abi dev (engine default); Docker Compose (engine default); Remote (engine default) | `sqlite3` |
| `postgresql` | PostgreSQL | A backend every engine reaches | — | `psycopg` |

Also accepts `nats_rpc` and `custom`.

Depends on:

- [Bus](/architecture/services-and-adapters#bus) (every adapter): broadcasts each published event on the engine's bus, wired at startup.
- [Dataset](/architecture/services-and-adapters#dataset) (`postgresql` only): moves events older than archive_after_days to the Dataset service (hourly job, NATS mode).

### Activity log

Records activity events; a failed record never breaks the call that produced it. Config: `services.activity_log.activity_log_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `sqlite` | SQLite | On this host (SQLite under storage/activity_log) | abi dev (without NATS, engine default); Docker Compose (without NATS, engine default); Remote (without NATS, engine default) | `sqlite3` |
| `document` | The engine's Document service | Wherever the document service keeps its data. | abi dev (engine default); Docker Compose (engine default); Remote (engine default) | — |

Also accepts `nats_rpc` and `custom`.

Depends on:

- [Document](/architecture/services-and-adapters#document) (`document` only): the engine's Document service, wired at startup.

### Dataset

Named tables with a partition spec, queried with SQL. Config: `services.dataset.dataset_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `ducklake` | DuckLake (DuckDB) with a SQLite or PostgreSQL catalog | On this host (DuckLake catalog sqlite:storage/datasets.sqlite; DuckLake data under storage/datasets/) | abi dev; Docker Compose; Remote (engine default) | `duckdb` |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Object storage

Stores files and binary objects under prefixes. Config: `services.object_storage.object_storage_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `fs` | Local disk | On this host (files under storage/datastore) | abi dev | — |
| `s3` | Amazon S3 or an S3-compatible store (MinIO) | A backend every engine reaches | Docker Compose; Remote | `boto3` |
| `naas` | Naas workspace storage | A backend every engine reaches | — | `boto3`, `requests` |
| `r2` | Cloudflare R2 | A backend every engine reaches | — | `boto3` |

Also accepts `nats_rpc` and `custom`.

Depends on:

- [Event](/architecture/services-and-adapters#event) (every adapter): publishes an ObjectPut or ObjectDeleted event on the engine's Event service for every write, wired at startup.

### Key-value

Stores keys and values, with expiry. Config: `services.kv.kv_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `redis` | Redis | A backend every engine reaches | Docker Compose; Remote | `redis` |
| `python` | In-process dictionary, optionally persisted to SQLite | On this host (this process, persisted to storage/kv/python.sqlite3) | abi dev | `sqlite3` |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Cache

A tiered cache: one adapter per tier (hot before cold), each entry in the adapters list. Config: `services.cache.adapters` (a list).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `fs` | Local disk | On this host (files under storage/cache) | — | — |
| `redis` | Redis | A backend every engine reaches | abi dev (hot tier, engine default); Docker Compose (hot tier, engine default); Remote (hot tier, engine default) | `redis` |
| `object_storage` | The engine's Object storage service | Wherever the object_storage service keeps its data. | abi dev (cold tier, engine default); Docker Compose (cold tier, engine default); Remote (cold tier, engine default) | — |
| `keyvalue` | The engine's Key-value service | Wherever the kv service keeps its data. | — | — |

Also accepts `nats_rpc` and `custom`.

Depends on:

- [Object storage](/architecture/services-and-adapters#object-storage) (`object_storage` only): the engine's Object storage service, wired at startup.
- [Key-value](/architecture/services-and-adapters#key-value) (`keyvalue` only): the engine's Key-value service, wired at startup.

### Bus

Module jobs and discovery also run on JetStream in NATS mode, but they use the broker directly, not the `bus` service.

Publishes and delivers messages on topics between services and modules. Config: `services.bus.bus_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `rabbitmq` | RabbitMQ (legacy, projects without NATS) | Not tracked by the engine | Docker Compose (without NATS); Remote (without NATS) | `pika` |
| `python_queue` | In-process queue, optionally persisted to SQLite | Not tracked by the engine | abi dev (without NATS) | `sqlite3` |
| `nats_jetstream` | NATS JetStream | Not tracked by the engine | abi dev (NATS mode, no block); Docker Compose (NATS mode, no block); Remote (NATS mode, no block) | `nats-py` |

- `rabbitmq`: Legacy, for projects without NATS, to be removed in the next major. Needs the naas-abi-core[rabbitmq] extra.
- `python_queue`: Projects without NATS. In NATS mode the engine refuses any bus adapter but nats_jetstream.
- `nats_jetstream`: In NATS mode leave the bus block out: the engine runs JetStream on nats.nats_url. An explicit block must name nats_jetstream and the same URL.

Also accepts `custom`. No `nats_rpc`: there is no NATS client for this service.

Depends on no other engine service.

### Secret

The first adapter in the list that has a value wins.

Resolves secrets across a list of adapters, and renders the {{ secret.NAME }} references in config.yaml. Config: `services.secret.secret_adapters` (a list).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `dotenv` | A .env file | A backend every engine reaches | abi dev; Docker Compose; Remote (the template's block does not validate) | `python-dotenv` |
| `naas` | The Naas secrets API | A backend every engine reaches | — | `requests` |
| `base64` | A base64-encoded .env held by another secret adapter | A backend every engine reaches | — | `python-dotenv` |

Also accepts `nats_rpc` and `custom`. `nats_rpc`: Accepted by validation, but SecretAdapterConfiguration.load() has no nats_rpc branch at this commit: loading it raises Unknown adapter.

Depends on:

- [Secret](/architecture/services-and-adapters#secret) (`base64` only): wraps another secret adapter, given in its secret_adapter block.

### Email

Sends email. Config: `services.email.email_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `smtp` | An SMTP server | A backend every engine reaches | abi dev (engine default); Remote (engine default) | `smtplib` |
| `filesystem` | .eml files on local disk | On this host (files under storage/email) | Docker Compose | — |
| `ses` | Amazon SES | A backend every engine reaches | — | `boto3` |
| `sendgrid` | SendGrid | A backend every engine reaches | — | `requests` |
| `microsoft_outlook` | Microsoft Outlook through the Microsoft Graph API | A backend every engine reaches | — | `msal`, `requests` |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Source control

Git repositories and in-app code review. Config: `services.source_control.source_control_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `forgejo` | Forgejo | A backend every engine reaches | — | `requests` |
| `in_memory` | In-process memory | On this host (this process) | abi dev (engine default); Docker Compose (engine default); Remote (engine default) | — |
| `local_git` | Git repositories on local disk | On this host (repositories under storage/repositories) | — | — |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

### Coding environment

Per-user coding workspaces. Config: `services.coding_environment.coding_environment_adapter` (one entry).

| `adapter:` | Backend | Keeps its data | Preset default | Client library |
|---|---|---|---|---|
| `coder` | Coder | A backend every engine reaches | — | `requests` |
| `code_server` | One code-server declared as a Docker Compose service | A backend every engine reaches | — | — |
| `in_memory` | In-process memory | On this host (this process) | abi dev (engine default); Docker Compose (engine default); Remote (engine default) | — |
| `local_directory` | Workspaces in directories on local disk | On this host (workspaces under storage/workspaces) | — | — |

Also accepts `nats_rpc` and `custom`.

Depends on no other engine service.

## Another engine's service: `nats_rpc`

In NATS mode one engine per NATS account serves the kernel services (`nats.engine.role: serve`, the default). It holds a lease: another serving engine fails to start, unless its `rollout_id` (`ABI_ROLLOUT_ID`) differs, in which case it stands by and takes over when the first one stops. Such a takeover needs every service on a shared backend; the engine refuses to start a rollout whose services keep data on its host. An engine with `role: client` never serves (CLI commands use `role: auto`: serve when no engine does). The serving engine exposes every service it loaded through that service's NATS primary adapter.

Any other process can use one of those services with `adapter: nats_rpc`. It then holds no data for that service: every call is a request to the serving engine, signed with a token from `jwt_secret`. A service that is itself a `nats_rpc` client is not exposed again.

```yaml title="config.yaml (a worker)"
services:
  object_storage:
    object_storage_adapter:
      adapter: "nats_rpc"
      config:
        nats_url: "nats://abi:{{ secret.NATS_ABI_PASSWORD }}@nats:4222"
        jwt_secret: "{{ secret.NATS_JWT_SECRET }}"
        service_identity: "worker"
        timeout_seconds: 10
```

- `jwt_secret` is required (at least 32 characters). `nats_url` defaults to `nats://127.0.0.1:4222`, `service_identity` to `engine`, `timeout_seconds` to 10.
- The bus has no `nats_rpc`: in NATS mode it is the broker's JetStream already.
- In NATS mode the cache tiers are either all `nats_rpc` or none: the full tier list lives on the engine that owns the cache.
- A secret entry with `nats_rpc` passes validation, but the pinned engine cannot load it (no loader branch), so do not use it yet.

## Your own adapter: `custom`

Every service accepts `adapter: custom`, which loads a class of yours through `GenericLoader`:

```yaml title="config.yaml"
services:
  document:
    document_adapter:
      adapter: "custom"
      python_module: "my_package.adapters.MyDocumentAdapter"
      module_callable: "MyDocumentAdapter"
      custom_config:
        url: "https://docs.example.com"
      shared_storage: true
```

1. Write a class that implements the service's adapter interface, for example `IDocumentAdapter` (`DocumentPort.py`) or `IObjectStorageAdapter` (`ObjectStoragePort.py`). The shipped adapters next to the interface are working examples.
2. Make it importable where the engine runs: a package in your project or an installed dependency.
3. Name it in `config.yaml`: `python_module` is the module path, `module_callable` the class (or a factory function), and `custom_config` the keyword arguments it is called with (`{}` when there are none). All three are required.
4. Set `shared_storage: true` when the adapter keeps its data in a backend every engine reaches. Without it the engine counts the data as local to this host, and a deploy cannot take over a running engine without downtime. A bus entry accepts the field, but it has no effect: the engine never asks the bus where its data lives.

`GenericLoader` imports `python_module`, takes `module_callable` from it and calls it with `custom_config` as keyword arguments. The document service also checks that the result is an `IDocumentAdapter`; the other services take it as is, so a missing method fails at the first call that needs it. A custom cache entry also takes a `tier`.

## Related

- [Architecture builder](/architecture/builder): pick an adapter per service, get the diagram and the `services:` block.
- [Data plane](/architecture/data-plane): where each kind of data lives.
- [The stack](/architecture/the-stack): the open-source libraries behind each adapter.
- [Configuration Reference](/capabilities/get-started/configuration#services): every `services:` block and its settings.
- [Real-time fabric](/architecture/real-time-fabric): NATS, JetStream and the subjects the primary adapters serve.
