A service module is compiled on the server side. It declares the service name and marks callable functions with @rpc.

defmodule MyApp.AdminRPC do
  use SafeRPC, service: :my_app, version: "1"

  @rpc true
  @doc "Return service status."
  @spec status(map(), map(), keyword()) :: {:ok, :ready | :degraded}
  def status(_payload, _meta, state) do
    {:ok, Keyword.get(state, :status, :ready)}
  end

  def helper, do: :not_exposed
end

Only public functions marked with @rpc true are exposed. @rpc functions must have arity 3:

(payload, meta, state)
  • payload is the request term.
  • meta is per-request metadata sent by the client.
  • state is the state returned by the service init/1 callback.

Operation identity is native BEAM data:

{MyApp.AdminRPC, :status}

Serve the service through the adapter server:

defmodule MyApp.AdminRPCServer do
  use SafeRPC.Adapter.Server, service: MyApp.AdminRPC
end

{:ok, _pid} = MyApp.AdminRPCServer.start_link(socket: "/run/my-app/rpc.sock")

use SafeRPC also implements SafeRPC.Adapter.Service. Override init/1 if the service needs runtime state:

def init(opts), do: {:ok, Keyword.fetch!(opts, :repo)}

Compile-time metadata

At compile time SafeRPC records:

  • service name and version;
  • exposed operation modules/functions;
  • docs and specs for descriptors;
  • atoms visible at RPC boundaries for safe ETF clients.

See Atom vocabularies and safe ETF.