What goes in changeset params vs the struct

You could expose multiple changeset handling functions. One which allows (a.k.a. casts) the roles, one which doesn’t. Schema.changeset/2 is only special in that it’s used as default changeset function for cast_assoc / cast_embed, but even these can be changed.

Another option would be using custom changesets for your UI layer (embed schema or even schemaless) and convert the validated data of that changeset into the actual params to pass further down into your system.