Koa: why rbac-fs reads the user from ctx.state
rbac-fs/koa's rbacMiddleware is the same function signature as the Express adapter — rbacMiddleware(rbac, resource, action, options?) — composed the same async/await-native way Koa middleware already works. The one real difference is where it reads the current user from.
ctx.state, not ctx.req
import { rbacMiddleware } from 'rbac-fs/koa';
app.use(async (ctx, next) => {
const role = ctx.get('x-user-role');
if (role) ctx.state.user = { id: 'demo-user', role };
await next();
});
const approveInvoice = rbacMiddleware(rbac, 'invoice', 'approve');Koa's own docs designate ctx.state as "the recommended namespace for passing information through middleware and to your frontend views" — it's explicitly not ctx.req (Koa wraps Node's raw request/response rather than extending them the way Express does). rbac-fs follows that convention rather than inventing its own, so it composes cleanly with whatever auth middleware you're already using (koa-jwt, koa-passport, a custom session middleware) as long as that middleware also writes to ctx.state.user.
Composes with any router
// works identically with koa-router, @koa/router, or no router at all
router.post('/invoices/:id/approve', approveInvoice, async (ctx) => {
ctx.body = { approved: ctx.params.id };
});rbacMiddleware's return value is just a normal Koa middleware function — nothing router-specific about it, so it slots into whatever routing setup your app already has without an adapter-specific integration step.
For a runnable, request-by-request walkthrough, see Tutorial: guarding a Koa route with rbac-fs.