Caravela supports row-level multi-tenancy via a single DSL flag:

defmodule MyApp.Domains.Library do
  use Caravela.Domain, multi_tenant: true

  entity :books do
    field :title, :string, required: true
    # tenant_id is auto-injected — don't declare it
  end
end

What changes

Schemas & migrations. A :tenant_id :binary_id column (null: false) is added to every entity. Migrations add composite [:tenant_id, :<fk>] indexes alongside each foreign-key index, plus a standalone [:tenant_id] index on tables with no FKs.

Context. The generated context scopes every read with where(q.tenant_id == ^tenant_id) and stamps every create with put_change(:tenant_id, tenant_id) — both driven by context.tenant.id at the call site. When the caller's context has no :tenant, the scoping helpers no-op (useful for background jobs that deliberately cross tenants).

Controllers & LiveViews. Generated controllers read conn.assigns[:tenant] into the context automatically. Same for generated LiveViews reading socket.assigns[:tenant]. You plug the tenant in ahead of your :api or :browser pipeline — from a subdomain, header, or session claim — Caravela only consumes it.

GraphQL. The Absinthe input objects hide tenant_id; tenant id comes from the resolver's Absinthe context, not the client.

Why row-level (and not prefix-based)

Ecto's prefix-based multi-tenancy requires per-tenant migration runs, repo reconfiguration, and is hard to debug. Row-level is explicit, standard, and works with a single database schema.

The entire multi-tenant machinery is one compile-time injection (Caravela.Tenant.inject/1) plus three lines in the context template. If you need to eject, it's trivial to remove.

Compile-time guard

If an entity tries to declare a :tenant_id field manually in a multi_tenant: true domain, the compiler raises:

** (CompileError) Caravela: entity :books declares a :tenant_id field,
but the domain has multi_tenant: true  tenant_id is auto-injected.

Caveats

  • Caravela does not enforce tenant isolation at the DB level (no RLS). It's the application layer's responsibility to pass the right tenant into the context. Scoping helpers make this impossible to forget for generated call paths, but anything you hand-write has to opt in.
  • The composite indexes assume tenant queries will be the common case. If you run a lot of cross-tenant admin queries, you may want extra indexes on [:<fk>] alone; add them in a follow-up migration.