FerricStore Elixir SDK

Elixir SDK for FerricStore and FerricFlow over the native ferric:// protocol.

Status: public alpha 0.1.0. APIs may change before 1.0, but the SDK is covered by command-construction tests, architecture tests, Docker-backed integration tests, and local benchmark scripts.

FerricFlow keeps each workflow or job's state and history in one durable place. It is an explicit durable state pipeline, not a hidden deterministic replay engine:

create -> claim -> handler -> transition/complete/retry/fail

Handlers should be idempotent because work can be retried after lease expiry, worker crash, or explicit retry.

Durability is the default contract. A workflow command returns success only after the state change is accepted by FerricStore and written through its durable path.

First 10 minutes

1. Install

def deps do
[
{:ferricstore_sdk, "~> 0.1.0"}
]
end

For local SDK development:

mix deps.get
mix test

2. Start FerricStore

For local development, use the Docker image:

docker run --rm \
-e FERRICSTORE_PROTECTED_MODE=false \
-p 6388:6388 \
ghcr.io/ferricstore/ferricstore:0.5.2

The SDK examples assume:

ferric://127.0.0.1:6388

3. Connect

{:ok, client} = FerricStore.start_link(url: "ferric://127.0.0.1:6388")
:ok = FerricStore.set(client, "hello", "world")
"world" = FerricStore.get(client, "hello")

4. Create a durable queue item

queue = FerricStore.Queue.new(client, "email", worker: "email-worker")
FerricStore.Queue.enqueue(queue, "email-1",
payload: "welcome:user-1",
attributes: %{tenant: "acme", campaign: "summer"}
)

Attributes are small indexed metadata. They are useful for search, filtering, and debugging. They are not payload bytes.

5. Process one queue batch

FerricStore.Queue.run_once(queue, fn job ->
send_email(job["payload"])
"sent"
end)

run_once/3 claims due work and completes or fails the job based on the handler result. For a long-running worker, call it from a supervised process with your own shutdown and concurrency policy.

6. Create a workflow/state machine

Use workflows when one durable flow moves through named states.

workflow = FerricStore.Workflow.new(client, "order", initial_state: "created")
FerricStore.Workflow.start(workflow, "order-1",
payload: "order payload",
attributes: %{tenant: "acme"},
values: %{order: :erlang.term_to_binary(%{total: 120})}
)

Claim, transition, and complete explicitly:

[job | _] = FerricStore.Workflow.claim(workflow, "created", limit: 1)
FerricStore.Workflow.transition(workflow, job["id"], "running", "charged",
partition_key: job["partition_key"],
lease_token: job["lease_token"],
fencing_token: job["fencing_token"],
payload: "charged"
)
[job | _] = FerricStore.Workflow.claim(workflow, "charged", limit: 1)
FerricStore.Workflow.complete(workflow, job["id"],
partition_key: job["partition_key"],
lease_token: job["lease_token"],
fencing_token: job["fencing_token"],
result: "ok"
)

After claim_due, the current durable state is running; the original claimed state is tracked as run state. Pass from_state: "running" when transitioning a claimed job.

7. Store and fetch named values

Use named values/value refs when different states need different pieces of data. Values are only hydrated when requested.

meta = FerricStore.Flow.value_put(client, "large invoice bytes",
owner_flow_id: "order-1",
name: "invoice_pdf",
override: false
)
ref = meta["ref"]
["large invoice bytes"] = FerricStore.Flow.value_mget(client, [ref])

Keep override: false for normal first-write values. Use override: true only when replacing a value is intentional.

8. Inspect state and history

record = FerricStore.Flow.get(client, "order-1", payload: true)
history = FerricStore.Flow.history(client, "order-1")

History is for debugging and audit. Handlers should use claimed job data and requested values, not history replay.

What you use

Production shape

Use one process/service to create work and a separate long-lived worker service to claim and complete work.

Phoenix/API/serverless producer -> FerricStore -> supervised worker service

Before production, configure timeouts, lease duration, backpressure behavior, graceful shutdown, and value hydration caps. The ferric:// transport uses one multiplexed native socket per SDK client process; create more client processes only after profiling shows client-side saturation.

Docs

Integration tests

Integration tests are explicit ExUnit integration tests. They run against the same Docker image used by CI:

docker run --rm \
-e FERRICSTORE_PROTECTED_MODE=false \
-p 6388:6388 \
ghcr.io/ferricstore/ferricstore:0.5.2
mix test --only integration