← All posts
NOV 2025

Uni Ecto Plugin

Yes, there's a Slack CLI. SlackCLI is an open-source command-line tool to read and send Slack messages from your terminal — built for AI agents and automation.

SlackCLI terminal interface showing Slack conversations
On this page

Uni Ecto Plugin

Before we touch a single line of Ecto code, we must understand Uni. Uni is not a framework—it is a design pattern with a reference implementation.

At its core, Uni solves a simple problem: How do we structure business logic without leaking implementation details?

Traditional Phoenix contexts often mix concerns:

Uni introduces the concept of Steps. A step is a single, pure (or controlled-effect) unit of work. Steps are composed into Pipelines. Uni guarantees that if a step fails (returns :error, term), the pipeline halts immediately—similar to a with statement but on steroids.

The Uni Ecto Plugin provides a set of pre-built, reusable steps specifically for database operations. Instead of writing Repo.insert(user_changeset) wrapped in a custom function, you call Uni.Ecto.insert() as a step in your pipeline. uni ecto plugin


A typical UNI is represented as:

uni://<type>/<origin>/<local_id>

Example:
uni://customer/stripe/cus_123xyz

Components:

# lib/uni/ecto/type.ex
defmodule UNI.Ecto.Type do
  @behaviour Ecto.Type

def type, do: :string

def cast(uni) when is_binary(uni) do case UNI.parse(uni) do {:ok, %UNI{} = uni_struct} -> :ok, uni_struct _ -> :error end end def cast(%UNI{} = uni), do: :ok, uni def cast(_), do: :error

def load(data) when is_binary(data), do: cast(data) def dump(%UNI{} = uni), do: :ok, UNI.to_string(uni) def dump(_), do: :error end

def all_tenants do # Could be a DB query or a static list ["public", "tenant_customer_a", "tenant_customer_b"] end end Before we touch a single line of Ecto

We plan to evaluate Uni Ecto Plugin against three criteria:

| Criterion | Vanilla Ecto | Uni Plugin | Notes | |-----------|--------------|------------|-------| | Lines of code for 4 plugins (soft-delete, encrypt, audit, tenant) | ~240 LOC | ~60 LOC | Across 5 schemas | | Plugin conflict resolution | Manual | Ordered priority | User defines order | | Query composition (e.g., soft-delete + tenant) | Manual where | Automatic via plugin chain | modify_query composes | | Runtime overhead | 0 | <5% | Macro-expanded per plugin |


Error: (Ecto.NoResultsError) expected query to return a prefix, but none was set Fix: Ensure your TenantResolver plug runs before any database calls in your controller pipeline.