5. Decide who can do what
Our back office works, but it’s wide open: anyone who can reach it can register authors and add books. The last thing a real app needs is to decide who is allowed. We’ll add role-based authorization — and the nice part is that it’s declarative, so none of our Handle() methods or read models change. The rule lives at the boundary, where it belongs.
Gate the commands
Section titled “Gate the commands”Authorization is a cross-cutting concern. You don’t want if (user.IsLibrarian) checks scattered through your logic — you want one attribute at the edge. Put [Roles(...)] on the command, and Arc checks the caller’s roles before the command runs:
-
Require a role to change the catalog. Only a librarian may register authors or add books:
[Command][Roles(nameof(UserRole.Librarian))]public record RegisterAuthor(AuthorId Id, AuthorName Name){public Task Handle(IMongoCollection<Author> authors) =>authors.InsertOneAsync(new Author(Id, Name));}The same attribute goes on
AddBook. Everything insideHandle()stays exactly as it was — the gate is applied around it.
Gate the queries
Section titled “Gate the queries”The read side deserves the same protection — a public catalog might be fine, but an internal audit list is not. Query methods take the same attribute:
[ReadModel]public record Author([property: Key] AuthorId Id, AuthorName Name){ [Roles(nameof(UserRole.Librarian))] public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) => collection.Observe();}Where the roles come from
Section titled “Where the roles come from”UserRole.Librarian is just a value you check against; the roles themselves come from the authenticated identity. Arc plugs into standard ASP.NET Core authentication, and you can enrich the identity with application-specific details — roles, tenant, preferences — through IProvideIdentityDetails. For local development you can generate a principal so you can exercise authorized endpoints without standing up a full login.
That’s a topic in its own right; the Identity section walks through it, and the Authorizing commands and queries recipe is the condensed how-to.
Look back at what you built
Section titled “Look back at what you built”Five chapters ago we had nothing. Now we have a real full-stack library back office, running over a plain database — and you’ve used every part of the Arc loop and know why each one is shaped the way it is:
- Chapter 1 — a full-stack slice: a
[Command]withHandle()that writes, a[ReadModel]with its query, and a React screen calling both through generated, typed proxies. - Chapter 2 — trust: a
ConceptValidatoron the value type and aCommandValidatorbusiness rule, both surfaced in the UI through the generated proxy. - Chapter 3 — relationships: a second slice and a filtered query that reads one author’s catalog, no join.
- Chapter 4 — life: observable queries as standing subscriptions, database to browser.
- Chapter 5 — authorization:
[Roles(...)]at the boundary, on both commands and queries, tied to the signed-in user.
The thing worth taking with you is the loop itself: something happens (a command) → it writes state → that state is served (a query) → and React calls all of it, typed end to end. Every feature you build from here is another turn of that loop in its own folder. You never go back and edit the last one to add the next.
And notice the boundary you proved: Arc did not need an event store for this CQRS loop. That is not an argument against event sourcing. For information systems, we usually reach for Chronicle as the default because history pays for itself. The point is architectural independence: CQRS can run over a plain database, and event sourcing can run without Arc, but they fit together extremely well.
From here, build another feature the same way, or take the next layer up: Components turns these generated commands and queries into higher-level screens, and the Cratis Stack tour shows where Arc fits with the rest of the platform.