Get started with Arc for Elixir
In this guide you build an authors feature: register an author, list authors live, and see the same rules apply over HTTP, in LiveView and in a test. By the end, posting a command to your Phoenix app records an event in Chronicle and the list updates without a refresh.
The finished code is in the Phoenix sample; each step below is excerpted from it.
Prerequisites
Section titled “Prerequisites”- Elixir 1.18 or later and a Phoenix 1.7+ project (a plain Plug application works too; see Plug).
- Docker, for a local Chronicle kernel.
- Familiarity with the Chronicle Elixir client; Arc builds on it.
Start Chronicle
Section titled “Start Chronicle”docker pull cratis/chronicle:latest-developmentdocker run -d --name chronicle \ -p 127.0.0.1:35000:35000 \ -p 127.0.0.1:27017:27017 \ cratis/chronicle:latest-developmentInstall
Section titled “Install”Add Arc to deps in mix.exs. It brings cratis_chronicle with it.
defp deps do [ {:cratis_arc, "~> 0.1"} ]endDefine the events and the read model
Section titled “Define the events and the read model”Events are Chronicle event types. The read model is a Chronicle projection: Chronicle keeps it current from the events, so Arc never writes it.
defmodule Library.Authors.AuthorRegistered do @moduledoc "An author was registered."
use Chronicle.Events.EventType, id: "library-author-registered"
defstruct name: ""
# A store-level rule: no two authors may share a name. Chronicle enforces it at append time, # and Arc reports a violation to the client as a `constraintViolation` validation result. unique :name, message: "An author with that name is already registered."enddefmodule Library.Authors.Author do @moduledoc "An author as the application shows it. Chronicle keeps it current from the events."
use Chronicle.ReadModels.ReadModel
defstruct id: "", name: ""
from Library.Authors.AuthorRegistered, set: [id: :event_source_id, name: :name] from Library.Authors.AuthorRenamed, set: [name: :name]endDefine a command
Section titled “Define a command”The command at the top of the overview is the whole write side. Three things to notice:
schemadeclares the request. Thekey: truefield is the event source: whateverhandle/2appends goes to that id.- Declarative options (
required,min_length,max_length) are validated by the server and, with proxies, by the browser. handle/2returns{:append, event}. It does not callChronicle.appenditself.
Define a query
Section titled “Define a query”A read model module holds the queries over one model. A query is a function that takes (args, ctx), annotated with @query or @observe.
@doc "All authors." @query many: true def all_authors(_args, ctx), do: Chronicle.all(Author, ctx.chronicle)ctx.chronicle carries the options Arc was configured with, for example which Chronicle client to use. Pass it on to Chronicle calls.
Start Arc
Section titled “Start Arc”Define the application module and add it to the supervision tree after the Chronicle client.
defmodule Library.Arc do @moduledoc "The Arc application: discovers the commands and read models under `Library`."
use Arc.Application, otp_app: :library endArc scans :library for modules that declare commands or read models. There is no registration list to keep in sync.
Serve it
Section titled “Serve it”Add Arc.Plug to the endpoint, before the router. Requests that are not Arc routes pass through.
plug Arc.Plug, application: Library.ArcNow run the app and post a command. Routes come from module names: with segments_to_skip: 1, Library.Authors.Registration.RegisterAuthor is POST /api/authors/registration/register-author.
mix phx.server
curl -s -X POST localhost:4000/api/authors/registration/register-author \ -H 'content-type: application/json' \ -d '{"id":"7f4d0f4e-0f4e-4d0f-8f4e-0f4e4d0f8f4e","name":"Ada Lovelace"}'{"correlationId":"…","isAuthorized":true,"isValid":true,"isSuccess":true,"hasExceptions":false,"validationResults":[],"exceptionMessages":[],"exceptionStackTrace":"","authorizationFailureReason":""}Send "name":"A" and the same call returns status 400 with a validation result for name, and nothing is recorded. That envelope is the Arc HTTP contract, the same one the C# and Kotlin implementations serve.
Read the list back:
curl -s localhost:4000/api/authors/listing/all-authorsChronicle projects asynchronously, so the author appears a moment after the command returns.
Use it from LiveView
Section titled “Use it from LiveView”LiveView calls the same modules in process. The page keeps the list current and shows validation errors as you type.
def mount(_params, session, socket) do # The signed-in user lives in the session; Arc reads the principal from the socket on every call. socket = if user = session["user"], do: assign(socket, :current_user, user), else: socket
{:ok, socket |> assign(:notice, nil) |> assign_identity() |> assign_observable(:authors, Authors, :live_authors) |> new_form()} endLiveView explains the rest.
Test it
Section titled “Test it”Arc.Test runs the real pipeline against Chronicle’s in-memory kernel, so you test behavior without a running kernel.
test "registering an author records AuthorRegistered on the author's event source", %{arc: arc} do id = Arc.new_id()
result = Arc.Test.execute(arc, {RegisterAuthor, %{"id" => id, "name" => "Ada Lovelace"}})
Arc.Test.assert_success(result) Arc.Test.assert_appended(arc, %AuthorRegistered{name: "Ada Lovelace"}, event_source_id: id) endYou declared a command and a query as modules, started Library.Arc, mounted Arc.Plug, and got an HTTP API, a LiveView page and tests from the same declarations. Nothing between the request and the event store is hand-written.
Next steps
Section titled “Next steps”- How Arc works explains discovery, the pipeline and the context.
- Building a React app? React and generated proxies is opt-in and adds typed clients.
- Commands and Queries cover every option.