Behaviour + use macro for the user-facing projection module.
See architecture doc §4.1 for the canonical example. The shortest form:
defmodule MyApp.Projections.Orders do
use Scriba.Projection,
name: "orders",
source: {Scriba.Source.Commanded, application: MyApp.CommandedApp},
target: {Scriba.Target.Ecto, repo: MyApp.Repo},
parallelism: 16
def handle(%OrderPlaced{} = event, _meta) do
{:insert, %OrderReadModel{id: event.order_id, ...}}
end
def handle(_event, _meta), do: :skip
endThen:
{:ok, _pid} = Scriba.start_projection(MyApp.Projections.Orders)Required options
:name— projection's logical identity. String. Per architecture §5, do NOT encode version in the name (e.g."orders_v1"); use the separate:versionoption for that. The macro emits a compile-time warning when:namematches_v<integer>$.:source—{module, opts}tuple. The module must implementScriba.Source(and Broadway'sProducer).:target—{module, opts}tuple. The module must implementScriba.Target.:parallelism— positive integer. Number of Broadway processors; also the modulus for the per-stream partition hash. No default — this is a real performance decision that should be deliberate.
Optional options (with defaults)
:version—1. Bump to run a new projection side-by-side with the old one (two modules, two version integers).:partition_by—:stream_id. Only:stream_idis supported in v0.1. Custom partitioners are post-v0.1; passing anything else raises at compile time.:handler—__MODULE__. The module whosehandle/2clauses the engine calls. Defaults to the module beingused.:batch_size,:batch_timeout,:retry— pass through to Pipeline and the retry layer respectively. SeeScriba.Projection.PipelineandSCRIBA_ARCHITECTURE.md§9.1 for defaults.
handle/2 callback contract
See architecture §4.2 for the six valid return shapes:
:skip{:insert, schema_struct}{:update, schema_module, filter, [set: changes]}{:delete, schema_module, filter}{:multi, %Ecto.Multi{}}{:error, reason}(routes to dead-letter)
Raising is also valid and treated like {:error, exception} (with the
exception struct and stacktrace preserved for the dead-letter row).
Retries apply to both shapes unless retry: false.
No catch-all is generated — decided, not overlooked
Commanded.Event.Handler generates def handle(_event, _metadata), do: :ok,
so events a projector did not match were silently ignored. Scriba generates
nothing: an unmatched event raises FunctionClauseError, dead-letters, and
advances the cursor.
On an :all subscription that means a migrated projector without a final
def handle(_event, _meta), do: :skip will dead-letter every unrelated
event in the system. That is loud and recoverable — the dead-letter table is
the first thing MIGRATION.md tells you to watch, and error_kind names
the cause — where the alternative fails silently and looks like a working
projection over an incomplete read model.
Loud is the v0.1 choice. It is also the one that stays open: an opt-in
on_unmatched: :skip can be added in any 0.1.x without breaking anyone,
whereas changing the default to skip would silently convert recorded
failures into nothing.
Summary
Callbacks
Handles a single event. The engine calls this once per event in per-stream order. See module doc for the valid return shapes.