Arex is an ArcadeDB-native Elixir client for applications that want a direct, idiomatic API over ArcadeDB's HTTP interface.
It is built around a small set of promises:
- plain Elixir maps in and out
- one-off function calls instead of a public client struct
- tenant and scope boundaries enforced by the high-level helpers
- normalized
{:ok, value}and{:error, error_map}return shapes - practical coverage for document, graph, schema, database, key/value, time-series, and vector workflows
Highlights
Arex.QueryandArex.Commandwrap raw query and command execution.Arex.Recordprovides document-style CRUD with tenant and scope awareness.Arex.VertexandArex.Edgecover graph creation and traversal.Arex.SchemaandArex.Databasehandle types, properties, indexes, buckets, and databases.Arex.KVwraps ArcadeDB's Redis-language key/value support over HTTP.Arex.TimeSeriescovers TimeSeries DDL, SQL helpers, and dedicated HTTP endpoints.Arex.Vectorwraps ArcadeDB dense, sparse, and hybrid vector search SQL patterns.Arex.Errorexposes stableerror.kindvalues for branching in application code.
Documentation Map
- Getting Started covers installation, configuration, first reads, and first writes.
- Records and Queries explains CRUD helpers, paging, batching, upserts, and when to drop to raw SQL.
- Graph and Schema documents provisioning, schema changes, graph helpers, and traversal patterns.
- Runtime Behavior explains option resolution, retries, timeouts, normalized errors, and observability expectations.
- AI Skill Guide summarizes safe usage rules for automation and agent workflows.
Installation
Add Arex to your dependencies:
defp deps do
[
{:arex, "~> 0.1.0"}
]
endThen fetch dependencies:
mix deps.get
Configuration
Arex resolves connection settings in this order:
- per-call options
- application config
- environment variables for
url,user,pwd, anddb
language works a little differently. It is resolved from call options or application config and otherwise defaults to "sql".
Recommended runtime.exs configuration:
import Config
config :arex,
url: System.fetch_env!("ARCADEDB_URL"),
user: System.fetch_env!("ARCADEDB_USER"),
pwd: System.fetch_env!("ARCADEDB_PASSWORD"),
db: System.fetch_env!("ARCADEDB_DATABASE"),
language: "sql"If you want Arex's built-in environment fallback, export these variables:
AREX_URLAREX_USERAREX_PWDAREX_DB
Common call options:
dbselects the ArcadeDB database when you do not want to use the configured default.typesupplies the record type for helpers such asArex.Record.get/2.tenantandscopedefine the application boundary for boundary-aware helpers.receive_timeoutsets the HTTP receive timeout in milliseconds.retryenables read retries with a value such as[max: 3, backoff_ms: 200].transactionandtransaction_timeouttune write helpers that need transactional behavior.headersmerges extra request headers without allowing callers to override auth headers.req_optionsmerges sanitized Req options after Arex strips retry settings that would bypass helper policy.
Quick Start
The following example assumes you have a running ArcadeDB server and want to store tenant-scoped customer records in a crm database.
alias Arex.{Query, Record, Schema}
# Run this once when bootstrapping an empty database.
{:ok, _} = Schema.create_document_type("Customer", db: "crm")
{:ok, _} = Schema.create_property("Customer", "external_id", :string, db: "crm")
{:ok, _} = Schema.create_index("Customer", ["external_id"], db: "crm", unique: true)
{:ok, customer} =
Record.persist(
%{external_id: "cust-1", name: "Ada Lovelace"},
db: "crm",
type: "Customer",
tenant: "ankara",
scope: "sales"
)
{:ok, same_customer} =
Record.fetch(customer["@rid"], db: "crm", tenant: "ankara", scope: "sales")
{:ok, page} =
Query.page(
"select from Customer where tenant = :tenant and scope = :scope order by @rid",
%{"tenant" => "ankara", "scope" => "sales"},
db: "crm",
limit: 25
)Boundary Model
Arex treats db, tenant, and scope as separate layers of isolation.
dbchooses the ArcadeDB database.tenantscopes records inside a database.scoperefines data inside a tenant and always requirestenant.
Boundary rules are consistent across the high-level APIs:
- insert-like helpers stamp
tenantandscopeinto written content when present - boundary-aware reads automatically filter by
tenantandscope Arex.KVnamespaces wrapped key helpers bytenantandscopeArex.TimeSeriesstampstenantandscopeas tags on boundary-aware writes and filters wrapped SQL/latest reads by those tags- attempts to mutate protected boundary fields through helper APIs are rejected
- cross-boundary access behaves as
:not_foundrather than leaking existence
Raw escape hatches such as Arex.KV.run/2, Arex.KV.batch/2, and hand-written TimeSeries SQL or PromQL remain caller-controlled.
This gives application code a stable model without repeating the same predicates in every call site.
Module Guide
| Module | Use it for |
|---|---|
Arex | connectivity checks and server metadata |
Arex.Query | raw reads, paging, and streaming |
Arex.Command | raw write commands and SQLScript execution |
Arex.Record | document-style CRUD helpers |
Arex.Schema | types, properties, indexes, and buckets |
Arex.Database | database creation, existence checks, and summaries |
Arex.KV | Redis-style key/value and hash helpers |
Arex.TimeSeries | TimeSeries DDL, ingestion, and query endpoints |
Arex.Vector | dense, sparse, and hybrid vector search helpers |
Arex.Vertex | vertex creation, updates, and traversal |
Arex.Edge | edge creation and lookups between vertices |
Arex.Error | normalized error maps returned by all helpers |
Common Workflows
Records And Queries
Use Arex.Record when you want the library to handle type resolution, boundary stamping, and common CRUD patterns for you.
{:ok, customer} =
Arex.Record.upsert(
"Customer",
%{name: "Ada Lovelace", status: "active"},
db: "crm",
where: %{external_id: "cust-1"},
tenant: "ankara",
scope: "sales"
)
{:ok, exists?} =
Arex.Record.is_there?(
%{external_id: "cust-1"},
db: "crm",
type: "Customer",
tenant: "ankara",
scope: "sales"
)
{:ok, first_row} =
Arex.Query.first(
"select from Customer where tenant = :tenant and scope = :scope order by @rid",
%{"tenant" => "ankara", "scope" => "sales"},
db: "crm"
)Use Arex.Query or Arex.Command when you need explicit control over the statement you send to ArcadeDB.
Graph Workflows
Use Arex.Vertex and Arex.Edge when your application works with graph types but you still want the same boundary semantics as document helpers.
{:ok, alice} =
Arex.Vertex.create(
"Person",
%{name: "Alice"},
db: "social",
tenant: "ankara",
scope: "graph"
)
{:ok, bob} =
Arex.Vertex.create(
"Person",
%{name: "Bob"},
db: "social",
tenant: "ankara",
scope: "graph"
)
{:ok, _edge} =
Arex.Edge.create(
"Knows",
alice["@rid"],
bob["@rid"],
%{},
db: "social",
tenant: "ankara",
scope: "graph"
)
{:ok, neighbors} =
Arex.Vertex.out(alice["@rid"], "Knows", db: "social", tenant: "ankara", scope: "graph")Schema And Database Administration
Use Arex.Database and Arex.Schema for setup, migrations, test provisioning, and operational inspection.
{:ok, :created} = Arex.Database.create("analytics")
{:ok, _} = Arex.Schema.create_document_type("Event", db: "analytics")
{:ok, _} = Arex.Schema.create_property("Event", "kind", :string, db: "analytics")
{:ok, _} = Arex.Schema.create_index("Event", ["kind"], db: "analytics")
{:ok, stats} = Arex.Database.stats(db: "analytics")For a deeper walkthrough, see Graph and Schema.
Key/Value Workflows
Use Arex.KV when you want Redis-style helpers over ArcadeDB's Redis-language
command surface without constructing raw command strings.
{:ok, "OK"} =
Arex.KV.set(
"session:ada",
"online",
db: "crm",
tenant: "ankara",
scope: "sales"
)
{:ok, "online"} =
Arex.KV.get(
"session:ada",
db: "crm",
tenant: "ankara",
scope: "sales"
)Wrapped key helpers namespace keys by active tenant and scope. Raw helpers
such as Arex.KV.run/2 and Arex.KV.batch/2 stay raw and do not rewrite
arbitrary Redis command strings.
Time-Series Workflows
Use Arex.TimeSeries when you want TimeSeries DDL, ingestion, and endpoint
helpers without hand-building /ts requests or raw create timeseries type
statements.
{:ok, _} =
Arex.TimeSeries.create_type(
"CpuMetric",
"ts",
[{"host", :string}],
[{"value", :double}],
db: "metrics",
tenant: "ankara",
scope: "ops"
)
{:ok, _} =
Arex.TimeSeries.insert(
"CpuMetric",
%{"ts" => 1_715_000_001_000, "host" => "app-1", "value" => 0.42},
db: "metrics",
tenant: "ankara",
scope: "ops"
)
{:ok, rows} =
Arex.TimeSeries.query_sql(
"select from CpuMetric where host = :host order by ts desc",
%{"host" => "app-1"},
db: "metrics",
tenant: "ankara",
scope: "ops"
)Structured TimeSeries helpers stamp tenant and scope as tags when present.
Raw SQL, PromQL, and raw payload endpoints remain available when you need full
control over the underlying ArcadeDB surface.
Vector Workflows
Use Arex.Vector when you want a typed wrapper around ArcadeDB vector indexes
and nearest-neighbor queries.
{:ok, _} = Arex.Schema.create_document_type("Doc", db: "search")
{:ok, _} = Arex.Vector.create_embedding_property("Doc", "embedding", db: "search")
{:ok, _} =
Arex.Vector.create_dense_index(
"Doc",
"embedding",
768,
db: "search",
similarity: :cosine
)
{:ok, neighbors} =
Arex.Vector.neighbors(
"Doc[embedding]",
[0.12, 0.34, 0.56],
10,
db: "search"
)The wrapper does not hide ArcadeDB vector concepts. It exists to make common index metadata and query construction easier to read and harder to get wrong.
Return Values And Errors
All public helpers return one of two shapes:
{:ok, value}{:error, error_map}
Example error:
{:error,
%{
kind: :not_found,
message: "record not found",
status: nil,
arcade_code: nil,
details: nil,
body: %{},
request: %{method: :post, path: "/api/v1/query/mydb"}
}}Important contract notes:
persist_multi/2runs inside one SQLScript transaction and is atomic.fetch_multi/2returnsnilentries for missing or out-of-boundary records.get_one/2returns{:ok, nil}for no rows and{:error, %{kind: :multiple_results}}for ambiguous matches.upsert/3requires a non-emptywhere:clause and fails when more than one row matches.Arex.Query.page/3acceptsoffset:at the Elixir API boundary, but internally emits ArcadeDBskipandlimitsyntax.
Runtime Behavior
Arex exposes a small, explicit set of transport controls:
receive_timeoutdefaults to60_000milliseconds when omitted.retryis disabled by default and is supported only on read helpers.- write helpers reject
retry:instead of silently retrying writes. req_optionsare sanitized before merge so callers cannot override helper retry policy.headerscan add request headers but cannot replace Arex's auth handling.
If you need the lower-level details, see Runtime Behavior.
ArcadeDB Compatibility Notes
Arex documents the ArcadeDB quirks it depends on rather than hiding them:
- generated pagination uses
skipandlimitbecause directoffsetSQL parsing failed in live ArcadeDB testing - non-unique index creation must emit ArcadeDB's explicit
notuniquekeyword - dropping bracketed index names such as
Customer[field]requires backtick quoting around the raw SQL index name - SQLScript scalar returns arrive through the HTTP API as rows such as
%{"value" => 5}rather than bare integers
Observability
Arex does not emit its own Telemetry events or logs. Instrumentation belongs at the application boundary:
- wrap Arex calls in your own logging, tracing, or telemetry spans
- use
error.request,error.status, anderror.detailswhen enriching logs - redact passwords, auth headers, and other secrets before logging inputs or failures
Local Development
The integration suite expects a live ArcadeDB server with an empty test_db database and the test_user account available. The official Docker image can provision that in one command:
docker run --rm -p 2480:2480 -p 2424:2424 \
-e 'JAVA_OPTS=-Darcadedb.server.rootPassword=root_password -Darcadedb.server.defaultDatabases=test_db[test_user:test_password]' \
arcadedata/arcadedb:latest
With the server running, export the values expected by local docs generation and the integration tests:
export AREX_URL=http://localhost:2480/
export AREX_USER=test_user
export AREX_PWD=test_password
export AREX_DB=test_db
Typical maintenance flow:
mix format
mix docs
mix test --cover
mix docs writes the generated site to doc/.