Skip to content

Extending Porulle

When Porulle’s bundled features don’t match your case, you extend the framework rather than fork it. Every extension point is a config-time contribution: hooks intercept operations, custom tables share the schema, plugins compose multiple extensions into one config-transform. No subclassing, no monkey-patching.

Custom routes must be classified as guarded or deliberately public so Porulle’s route coverage check can verify the application surface. Use requirePerm for a route that needs a permission, markRoutePermissionGuard when your own middleware performs the authorization, and markPublicRoute when a route is intentionally public.

import { markPublicRoute, markRoutePermissionGuard, requirePerm } from "@porulle/core";
routes: (app) => {
app.use("/api/orders", requirePerm("orders:read"));
app.use("/api/internal", markRoutePermissionGuard(myAuthorizationMiddleware));
app.use("/api/vendor-webhook", markPublicRoute(myPublicMiddleware));
}

markPublicRoute is the named public alias of the same route marker used by markRoutePermissionGuard; it makes the intent clear while keeping route coverage explicit.

These three contracts are normative — every extension must follow them:

  • Plugin Contract — what plugin authors MUST do (actor as required parameter, Result<T>, org scoping, no body-supplied identity)
  • Payment Adapter Contract — accurate amountCaptured, idempotency, timing-safe webhook verification
  • Security Model — the framework’s threat model and adopter-side responsibilities