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?
Example policy
Section titled “Example policy”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); }}Using policies in controllers
Section titled “Using policies in controllers”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.
Field-level visibility
Section titled “Field-level visibility”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.
Policy discipline
Section titled “Policy discipline”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.