CI

AshBoundary is a Spark DSL extension for Ash domains. It derives a boundary declaration from the domain DSL. The boundary compiler enforces the declaration on each build.

Every option in the boundary block passes straight through to boundary, unchanged: deps, exports, check, type, dirty_xrefs all mean exactly what they mean on a hand-written use Boundary. AshBoundary computes one thing on top: exports gains the domain module itself and every resource with at least one domain-level define.

Conventions

  • The domain module is public.
  • Each resource that exposes a code interface in the domain is public.
  • Each module named in the boundary block's exports is public.
  • All other modules in the domain's namespace are internal.
  • Referencing another domain requires an explicit boundary dep, subject to whatever check that domain declares.

Installation

Add ash_boundary and boundary to the deps in mix.exs:

def deps do
  [
    {:ash_boundary, "~> 0.1.0"},
    {:boundary, "~> 0.10", runtime: false}
  ]
end

Add the :boundary compiler to the project configuration:

def project do
  [
    app: :my_app,
    compilers: [:boundary] ++ Mix.compilers(),
    # ...
  ]
end

Usage

Add AshBoundary to the extensions of a domain. Declare dependencies on other domains, and any public module that is not a resource, in a boundary block:

defmodule MyApp.Blog do
  use Ash.Domain, extensions: [AshBoundary]

  boundary do
    deps [MyApp.Accounts]
    exports [MyApp.Blog.PostStatus]
  end

  resources do
    resource MyApp.Blog.Post do
      define :get_post, action: :read
      define :update_post, action: :update
    end

    resource MyApp.Blog.Comment
  end
end

This configuration has these effects:

  • MyApp.Blog.Post is public. All modules can reference it.
  • MyApp.Blog.PostStatus, an Ash.Type.Enum, is public.
  • MyApp.Blog.Comment is internal. Only modules in the MyApp.Blog namespace can reference it.
  • MyApp.Blog can depend on the MyApp.Accounts boundary only.

Examples

Three examples live in examples/, each its own Mix project:

  • 01_exported_vs_internal has two domains. It covers exported and internal resources, and a calculation that reads another domain's data through that domain's exported interface instead of a relationship.
  • 02_phoenix_liveview is a Phoenix application over two domains. Its web layer reaches both through their exported interfaces and cannot call Ash.*.
  • 03_tower stacks thirteen domains over one PostgreSQL database in named tiers: shared infrastructure, a core of pure-identity anchors, satellites that each attach a fact to an anchor, then derivation, orchestration, and read-projection tiers above them. Every dependency points down, so the graph cannot cycle back on itself, and each tier above the satellites exists because some question needs more than one satellite to answer it. The top tier composes the others' read actions into single SQL statements and serves them over JSON:API, delegating writes back down to the domains that own the data. Two Hologram front ends sit above that, a public storefront that takes no logins and a role-gated operations console, each its own boundary and its own endpoint, and neither able to reference the other.

Clarity

With clarity installed, each domain gains a "Boundary Dependencies" tab holding a diagram of its deps, where clicking a node opens that domain.

The diagram stacks domains in tiers by their distance from the domains that depend on nothing, and leaves out any edge a longer path already implies.

Usage rules

The package ships a usage-rules.md for usage_rules. List :ash_boundary in your project's usage_rules and run mix usage_rules.sync:

def project do
  [
    usage_rules: [:ash_boundary],
    # ...
  ]
end

Documentation

Docs for latest main is published at mbuhot.github.io/ash_boundary. Docs for versioned releases will be available at hexdocs.pm/ash_boundary.

See also the boundary docs.

License

MIT. See LICENSE.