Skip to content

Policies

SemitraPolicy is the authorization boundary in Semitra.

Policies answer three different questions:

  • is this action allowed?
  • what records are visible in this scope?
  • which fields are visible for this action?
import ApplicationPolicy from "./application_policy.ts";
type ExampleUser = {
id: string;
role: string;
};
export default class PostPolicy extends ApplicationPolicy<Post | typeof Post, ExampleUser | null> {
show(): boolean {
return true;
}
create(): boolean {
return true;
}
update(): boolean {
return true;
}
destroy(): boolean {
return this.user?.role === "admin";
}
}

That policy aligns with a typical controller flow:

import ApplicationController from "./application_controller.ts";
import Post from "./post.ts";
import PostPolicy from "./post_policy.ts";
export default class PostsController extends ApplicationController {
static {
this.verifyPolicyScoped({ only: ["index"] });
this.verifyAuthorized({
only: ["show", "create", "update", "destroy"]
});
}
async index() {
const Posts = this.model(Post);
const posts = await Posts.all();
const scopedPosts = await this.policyScope(PostPolicy, posts, Post);
return this.renderResourceCollection(scopedPosts);
}
async destroy() {
const Posts = this.model(Post);
const post = await Posts.find(String(this.params.get("id")));
await this.authorize(post, "destroy");
await post.destroy();
return this.head(204);
}
}

Typical controller usage:

  • await this.authorize(record, "show")
  • await this.authorize(Post, "create")
  • await this.policyScope(PostPolicy, posts, Post)

The example app adds verification hooks so each action fails if required policy work never ran.

Resources can ask a policy for permittedFields(action). This allows the resource layer to omit fields dynamically without duplicating authorization logic in serializers.

For example, a policy can expose fewer fields to non-admin readers while still using the same resource class:

permittedFields(action?: string) {
if (this.user?.role === "admin") {
return undefined;
}
if (action === "index") {
return ["id", "title", "createdAt", "excerpt"];
}
return ["id", "title", "content", "createdAt"];
}

That is useful when you want one serializer contract but different visibility rules for feed views, detail views, or admin-only fields.

Keep policies explicit:

  • do not hide authorization inside model queries
  • do not rely on controller branching alone for access rules
  • do not duplicate the same rule in multiple controllers

If an action needs authorization, a policy should own it.