Rift gives Phoenix apps a configurable LiveView ops inbox for workflows that need human decisions.
The host app defines case types in code. Users open cases through host-defined forms. Each case starts one Squid Mesh workflow run. Operators review cases in Rift, claim or assign ownership, approve/reject/cancel work, and inspect runtime details through SquidSonar.
Read the planning document:
Rift is built as an embeddable Phoenix package. The host app owns its Ecto repo, auth, actors, tenancy, case types, workflow modules, selectable values, file storage, and side effects.
Configure the host repo:
config :rift, repo: MyApp.RepoGenerate the host migration for Rift's case and event tables:
mix rift.install
mix ecto.migrateDefine host case types with use Rift.CaseType and expose host-owned context
through a Rift.Resolver implementation.
defmodule MyApp.CaseTypes.AccessChange do
use Rift.CaseType
case_type do
type :access_change
title "Access change"
description "Ask an operator to review an access change before it runs."
team "identity"
workflow MyApp.Workflows.AccessChange
trigger :submit
fields do
field :target_user_id, :select,
label: "User",
required: true,
options: {:resolver, :target_user_id}
field :role, :select,
label: "Role",
required: true,
options: [{"Operator", "operator"}, {"Admin", "admin"}]
field :reason, :textarea,
label: "Reason",
required: true
end
end
@impl true
def build_payload(attrs, ctx) do
%{
target_user_id: attrs.target_user_id,
role: attrs.role,
reason: attrs.reason,
opened_by: ctx.actor.id
}
end
endMount Rift in the host router:
defmodule MyAppWeb.Router do
use Phoenix.Router
use Rift.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
rift "/rift", otp_app: :my_app, resolver: MyApp.RiftResolver
end
scope "/" do
pipe_through [:browser, :require_authenticated_user]
rift_originator "/cases", otp_app: :my_app, resolver: MyApp.RiftResolver
end
endrift/2 mounts the operator inbox. rift_originator/2 mounts case submission
routes at /new and /new/:type under the path you choose, so the host can put
originator intake behind a different auth pipeline, a public signed-token plug,
or any other boundary it owns.
Resolve host-owned actors, tenancy, access, case types, and select options:
defmodule MyApp.RiftResolver do
@behaviour Rift.Resolver
@impl true
def resolve_actor(conn), do: conn.assigns.current_user
@impl true
def resolve_tenant(actor), do: actor.organization_id
@impl true
def resolve_access(_actor), do: :operator
@impl true
def resolve_case_types(_actor), do: [MyApp.CaseTypes.AccessChange]
@impl true
def resolve_select_options(_actor, MyApp.CaseTypes.AccessChange, :target_user_id) do
Enum.map(MyApp.Accounts.list_users(), &{&1.name, &1.id})
end
endKeep the Rift DSL formatted without parentheses by importing Rift in the host formatter config:
[
import_deps: [:rift],
inputs: ["{config,lib,test}/**/*.{ex,exs}"]
]Rift uses Phoenix and LiveView internally, but it should be mounted inside a host Phoenix application rather than operated as a standalone app.
mix setupBefore opening a pull request, run:
mix precommitSee CONTRIBUTING.md for the full contributor workflow,
including how to use examples/standalone for feature development and QA.
