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 route access
Section titled “Custom route access”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.
Adopter contracts
Section titled “Adopter contracts”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
Where to next
Section titled “Where to next”- Reference → Plugins —
defineCommercePluginmanifest reference - Reference → Hooks — every available hook key with type signatures
- Concepts → Plugin Architecture — why plugins are config transforms
- Tutorials → Build a Loyalty Plugin — a full plugin walkthrough