I’m proud to announce that I’ve finally finished polishing, open-sourcing and documenting the first version of Kilpi!
Kilpi [/ˈkilpi/] is the Finnish word for shield.
In this article, I will briefly introduce you to the “what”, “why” and the “how” of Kilpi.
- What is Kilpi?
- What motivated me to create Kilpi?
- How to use Kilpi?
- What design philosophies is Kilpi based on?
Say hello to Kilpi!
Kilpi is my solution to authorization in fullstack TypeScript applications.
Way too often, I’ve cooked the same spaghetti and ended up with unmaintainable code. Let’s say that you can delete a document if you’re the author, an application superadmin or you are an admin in the organization that the document belongs to. Most likely, you’d write something like the following.
async function deleteDocument(id: string) { const document = await db.getDocument(id);
if ( !user || user.role !== "superadmin" || user.id !== document.authorId || document.orgId ? (await listUserOrgMemberships(user.id)).some( m => m.orgId === document.orgId && m.roles.includes("admin") ) : false ) { // That was 🍝 throw new UnauthorizedException("You are not allowed to delete this document"); }
await db.deleteDocument(id);}
Oh. And remember to also hide the delete button in your UI for unauthorized users (Kilpi does this too).
Don’t you just wish that it would be as simple as
async function deleteDocument(id: string) { const document = await db.getDocument(id); await Kilpi.authorize("documents:delete", document); await db.deleteDocument(id);}
That’s what Kilpi is.
“But you’re just moving the spaghetti to another file?” — Thank you mr. Strawman. It’s called a centralized authorization layer which is a fantastic method to keep your codebase clean and maintainable.
And it’s completely free and open-source unlike the lock-in products by paid auth services, such as Auth0, Clerk or Kinde who offer their own UI-based, proprietary authorization solutions.
Ready, set, go!
Let’s look behind the curtains. This section will act as a quick setup guide for Kilpi, showing you how to implement the above document deletion protection.
-
Install
@kilpi/core
with your package manager of choice. -
Connect your auth provider by setting up a subject.
subject.ts import { getSessionFromMyAuthProvider } from "./my-auth-provider";export type Subject = { id: string, name: string }export async function getSubject() {const session = await getSessionFromMyAuthProvider();if (!session) return null;return { id: session.userId, name: session.userName };} -
Setup your policies and define the
documents:delete
policy.policies.ts import { deny, grant, type Policyset } from "@kilpi/core";import { Subject } from "./subject";function hasOrgRole(subject: Subject, orgId: string, role: OrgRole): Promise<boolean> { ... }export const policies = {documents: {async delete(user, doc: Document) {if (!user) return deny("Unauthenticated");if (user.role === "superadmin") return grant(user);if (user.id === doc.authorId) return grant(user);if (await hasOrgRole(user, doc.orgId, "admin")) return grant(user);return deny();}}} as const satisfies Policyset<Subject | null> -
Create your Kilpi instance.
kilpi.ts import { createKilpi } from "@kilpi/core";import { policies } from "./policies";import { getSubject } from "./subject";export const Kilpi = createKilpi({ policies, getSubject }); -
Protect your deletion.
document-service.ts import { Kilpi } from "./kilpi";async function deleteDocument(id: string) {const document = await db.getDocument(id);await Kilpi.authorize("documents:delete", document);await db.deleteDocument(id);}
And it doesn’t end there. Kilpi provides a comprehensive set of tools for protecting your applications since day 1. There’s protected queries, plugins, hooks, scopes, React Server Components and much more to explore in the documentation.
Design philosophy
Kilpi is based on several design philosophies that have proved themselves to be solid foundations for an authorization layer. In short (or in long):
- Server-first authorization: All authorization decisions are made on the server.
- Centralized authorization layer: All policies are centralized in one place.
- Throw-on-unauthorized: The primary API of Kilpi is to throw on unauthorized actions, however it is not the only way to use Kilpi.
- Protected queries: Kilpi introduces protected queries to protect your data in a safer way at the very source.
- Declarative API: Easier to maintain.
- Policies as code: Policies use a type-safe, flexible and familiar language and are included in your version control.
- Async policies: Policies may fetch data from external sources to make decisions.
- 100% Type safety: Everything in Kilpi is type-safe to the maximum for the best developer experience.
- Framework agnostic: You can use Kilpi with any server-side technology (e.g. Next, React RSC, Express, Koa, Oak, NestJS, Hono, and soon even on the client).
- Authentication provider agnostic: With the subject pattern, Kilpi encapsulates any authentication provider and ensures your authorization is not tied to your authentication.
- Authorization model agnostic: Kilpi supports all authorization models such as ABAC, RBAC, ReBAC, dynamic permissions or any custom model you can think of.
Conclusion
Kilpi will continue to develop to become the best authorization framework for TypeScript applications. New plugins, client-side authorization, new framework integrations and much more are already on the roadmap.
Please, provide critique for Kilpi and support development to help Kilpi, or follow the development of Kilpi on my Bluesky profile.
Get started
Whether you’re tired of reinventing authorization logic or just looking for a more elegant solution, Kilpi is here to help. Ready to simplify your code and secure your app?