Skip to main content

Food Delivery System Example

Run a Nuxt.js food delivery app and configure its authorization in Permit.io, one policy model at a time. This worked example is for developers who build with Nuxt or Vue and want to see role-based access control (RBAC), attribute-based access control (ABAC), and relationship-based access control (ReBAC) enforced together on the same action.

By the end, customers, vendors, riders, and admins in the app can each perform only the order and meal actions that the policy allows them.

Prerequisites

What you build

The app code is in the permit-nuxt-example repository, on the all-features branch. The app has four pages, one per role:

  • A customer page for browsing meals and placing orders
  • A vendor page for managing meals and fulfilling orders
  • A rider page for delivering orders
  • An admin page for assigning riders and managing the system

Tech stack

LayerWhat the app uses
FrontendNuxt.js with Vue, one page per role (customer, rider, vendor, admin)
BackendNuxt server routes with authorization middleware that calls permit.check()
AuthorizationRBAC for role permissions, ABAC for the free delivery offer, ReBAC for order assignments, and cities as tenants
Data syncServer routes that sync users, orders, and role assignments to Permit when they change

How the app uses each policy model

ModelWhat it controls in the app
RBACWhich roles can create, delete, and act on meals and orders
ABACFree delivery: orders that cost 500 or more, and riders with 500 or more rides
ReBACOrder assignments: the vendor who owns the order and the rider assigned to it

Order management

Each role acts on orders at a different stage:

  • Customers create orders.
  • Vendors fulfill orders (mark them ready for delivery).
  • Admins assign riders to orders.
  • Riders deliver orders.

Composite permissions

Policies from all three models apply to the same action. For the deliver action:

  • The rider role grants deliver on the Order resource type (RBAC).
  • The Order#Rider resource role links a rider to one order (ReBAC).
  • A user set of riders with 500 or more rides grants deliver on the resource set of free delivery orders (ABAC).

The deliver endpoint runs two checks: server/middleware/permissions/check-role.ts checks deliver on the Order resource type, and server/middleware/permissions/check-rider-eligibility.ts checks deliver on the specific order instance. Permit evaluates each check against every rule that applies to the action.

Multi-tenant authorization

The app uses cities (california and washington) as tenants. A role assignment in one city doesn't grant the same permission in the other city.

Continuous data syncing

The server routes sync these changes to Permit when they happen:

  • A customer creates an order.
  • An admin assigns a rider.
  • An order's status changes.

Server endpoints

The Nuxt server exposes these endpoints. The Authorization column lists the policy models that each endpoint's check depends on, and the Action column lists the action key the middleware derives from the method and path.

MethodEndpointActionAuthorization
GET/mealsread (not checked)None
POST/mealscreateRBAC
DELETE/meal/:iddeleteRBAC
GET/ordersread (not checked)None
POST/orderscreate, create-with-free-deliveryRBAC & ABAC
POST/order/:id/fulfillfulfillRBAC & ReBAC
POST/order/:id/assign-riderassign-riderRBAC
POST/order/:id/deliverdeliverRBAC, ABAC, & ReBAC
POST/usersNoneNone
DELETE/usersNoneNone

check-role.ts derives read for every GET request and then returns before calling permit.check(), with the comment "Allow the user to read meals and others even if they are not logged in". Reading meals and orders therefore succeeds for any caller, signed in or not. The read permission you grant in the Policy Editor documents the intent, and it feeds the frontend checks in permit-vue-example, but it changes nothing in this app.

A GET endpoint with no check returns data to anyone

/meals and /orders return every meal and every order in the tenant without a permission check. The example does this to keep the demo browsable before you build the policy. Remove the early return for read in check-role.ts before you reuse this middleware in an application with real data.

Set up and run the app

Clone the app, connect it to your Permit environment, start a local PDP, and open the app in your browser.

1

Clone the repository and install dependencies

git clone https://github.com/permitio/permit-nuxt-example.git
cd permit-nuxt-example
git checkout all-features
npm install
2

Set the environment variables

  1. Copy your environment API key from the API keys settings in Permit.
  2. Create a .env file at the root of the project with the following content. Replace the PERMIT_TOKEN placeholder with your API key.
PERMIT_TOKEN=permit_key_XXXXXXXXXXXXXXXXXXXXXXXXX
PERMIT_PDP=http://localhost:7766

nuxt.config.ts maps PERMIT_TOKEN to runtimeConfig.permitToken and PERMIT_PDP to runtimeConfig.permitPdp, and server/utils/permit.ts builds the Permit client from both.

Keep PERMIT_PDP on the container PDP

The sample.env file in the repository points PERMIT_PDP at https://cloudpdp.api.permit.io. The Cloud PDP does not evaluate ABAC, so the free delivery checks in Configure ABAC for free delivery return false against it. Set PERMIT_PDP=http://localhost:7766 and run the container PDP in the next step. See Cloud PDP capabilities.

3

Start a local PDP

Run the PDP container. Replace your-permit-api-key with your environment API key. The command maps the container's port 7000 to port 7766 on your machine, which matches PERMIT_PDP.

docker run -it \
-p 7766:7000 \
--env PDP_API_KEY=your-permit-api-key \
--env PDP_DEBUG=True \
permitio/pdp-v2:latest

For more PDP options, see Run the PDP.

4

Run the development server

In a new terminal, from the project root:

npm run dev
5

Open the app

Open http://localhost:3000 in your browser. The app shows a page for each role's actions. Use the sidebar to set the current user ID, set roles, choose the current city, and delete users.

Food delivery app with a sidebar for choosing the current user and role, and pages for customer, vendor, rider, and admin actions

Verify: the meal and order lists render, and placing an order answers "You are not permitted to perform this action". The empty Permit environment has no Order resource yet, so permit.check() returns false.

info

The meal and order lists load because GET /meals and GET /orders return before the middleware calls permit.check(). Every other action stays denied until you build the policies in the next three sections.

Configure RBAC for meals and orders

Role-based access control (RBAC) grants permissions by role. RBAC is the base layer of the app's policy:

Meals:

  • Only vendor and admin can create and delete meals.
  • All roles (customer, rider, vendor, and admin) get read, which the middleware does not check.

Orders:

  • Only customer can create orders.
  • Only vendor can fulfill orders (mark them ready for delivery).
  • Only admin can assign riders to orders.
  • Only rider can deliver orders.
  • admin can also create, fulfill, and deliver orders.
  • All roles get read, which the middleware does not check.

To configure these RBAC policies:

1

Create the Meal and Order resources

On the Policy screen, open the Resources tab. Create two resources, Meal and Order, with the action keys the app derives in its middleware and sends from its frontend. Type the keys exactly as listed, including the hyphens: permit.check() matches on the key, and a mismatch returns false.

ResourceAction keysWhere the key comes from
Mealcreate, read, deleteserver/middleware/permissions/check-role.ts maps POST to create, DELETE to delete, and everything else to read.
Ordercreate, read, fulfill, assign-rider, delivercheck-role.ts maps POST /orders to create and takes any other POST /order/:id/<action> path segment as the action key.

Add the create-with-free-delivery action on Order in Configure ABAC for free delivery.

Creating the Meal and Order resources and their actions on the Resources tab

2

Create the four roles

On the Roles tab, create the customer, rider, vendor, and admin roles.

Roles tab listing the customer, rider, vendor, and admin roles

3

Grant RBAC permissions in the Policy Editor

In the Policy Editor, check the actions each role may perform on Meal and Order, following the RBAC rules in this section.

Policy Editor with meal and order actions checked for each role

Verify: refresh the app, then in the sidebar set the current user and assign that user the customer role in the current city. Place an order on the customer page. The order appears in the order list. Switch the current user to one with the rider role only and place another order: the app answers "You are not permitted to perform this action".

Configure ABAC for free delivery

Attribute-based access control (ABAC) grants permissions based on attributes of the user and the resource. You group attribute conditions into user sets and resource sets, and grant permissions to those sets in the Policy Editor.

The app uses ABAC for a free delivery offer:

  • A customer can create an order with free delivery if the order's cost is 500 or more.
  • A rider can deliver a free delivery order if the rider's number_of_rides is 500 or more.

These ABAC rules extend the RBAC roles and actions with conditions.

1

Add the cost attribute and the free delivery action

  1. Open the Order resource for editing.
  2. Add a create-with-free-delivery action. The app checks this action when it decides whether to issue free delivery.
  3. In the ABAC section, add a cost attribute of type Number.
  4. Save the changes.

Editing the Order resource to add the create-with-free-delivery action and a cost attribute of type Number

2

Create a resource set for orders that cost 500 or more

  1. On the ABAC Rules tab, create a resource set for Order.
  2. Add the condition cost greater than or equal to 500.

Every order whose cost matches the condition belongs to the resource set.

3

Add the number_of_rides user attribute

In the user attributes settings, add a number_of_rides attribute of type Number.

Adding a number_of_rides user attribute of type Number

4

Create a user set for riders with 500 or more rides

  1. On the ABAC Rules tab, create a user set.
  2. Add the condition number_of_rides greater than or equal to 500.

Every user whose number_of_rides matches the condition belongs to the user set.

5

Grant ABAC permissions in the Policy Editor

The user set and the resource set appear in the Policy Editor.

  1. For the customer role, check create-with-free-delivery on the order resource set.
  2. For the rider user set, check deliver on the order resource set.

Policy Editor with the create-with-free-delivery and deliver actions checked for the ABAC user set and resource set

Verify: refresh the app and, as a customer, create an order whose meals total 500 or more. The order's Delivery Fee row reads FREE. Create an order under 500 and the row shows the fee, because create-with-free-delivery returns false and issue-free-delivery.ts leaves deliveryFee in place.

Configure ReBAC for order assignments

Relationship-based access control (ReBAC) grants permissions based on relationships between users and resource instances. The app uses ReBAC for two rules:

  • A vendor can fulfill an order only if the vendor created the meals in that order.
  • A rider can deliver an order only if an admin assigned the order to that rider.

The fulfill and deliver endpoints check the specific order instance, { type: 'Order', key: orderId, tenant }, so the PDP can evaluate the user's resource roles on that order. A resource role is a role that applies to instances of one resource type, written Resource#Role. See ReBAC overview.

1

Create the Vendor and Rider resource roles

  1. Open the Order resource for editing.
  2. In the ReBAC section, create two resource roles with the keys Vendor and Rider. The Policy Editor shows them as Order#Vendor and Order#Rider. The app assigns Vendor in server/routes/orders/index.post.ts and Rider in server/routes/order/[id]/assign-rider.post.ts, both by that exact key.

Creating the Order#Vendor resource role and checking the fulfill action for it in the Policy Editor

2

Grant ReBAC permissions in the Policy Editor

In the Policy Editor, check fulfill for Order#Vendor and deliver for Order#Rider.

Verify: refresh the app. Sign in as the vendor who created the meals in an order and fulfill it: the order's Fulfilled Time fills in. Switch to a vendor who created no meals in that order and fulfill it again: the app answers "You are not permitted to perform this action", because that vendor holds no Order#Vendor role on the instance.

Sync app data to Permit

The policies depend on users, orders, and role assignments that exist in Permit. The app syncs them with methods on permit.api from the Permit Node.js SDK.

The handlers of the /users endpoints create or update users with permit.api.users.sync(), which creates the user if the user doesn't exist and updates the user if the user exists. The number_of_rides attribute is sent with the user. The handlers delete users with permit.api.users.delete().

server/routes/users/index.post.ts and index.delete.ts
// Create the user if it does not exist, and update it if it does
await permit.api.users.sync({
key: user,
...(noOfRides ? { attributes: { number_of_rides: noOfRides } } : {}),
});

// Delete the user
await permit.api.users.delete(user);

The app also syncs resources and role assignments:

SDK namespaceWhat it manages
permit.api.resourcesResource types
permit.api.resourceInstancesIndividual resource instances, such as one order
permit.api.roleAssignmentsRole assignments, both tenant-level (RBAC) and resource instance-level (ReBAC)

Resource instances and role assignments belong to a tenant. Pass the tenant key, or "default" if your application is not multi-tenant. Permit also exposes condition sets through the Condition Sets API.

When a customer creates an order, the app creates the order as a resource instance carrying its cost, and assigns the Order#Vendor resource role on that order to the vendor. The cost attribute is what the ABAC resource set from Configure ABAC for free delivery reads. When an order is deleted, the app deletes the resource instance.

server/routes/orders/index.post.ts
// tenant is the city header on the request, for example "california"
const { city: tenant, id, totalPrice, vendor } = newOrder;

// Sync the new order with Permit
await permit.api.resourceInstances.create({
key: id,
resource: 'Order',
attributes: { cost: totalPrice },
tenant,
});

// Assign the Order#Vendor resource role on this order to its vendor
await permit.api.roleAssignments.assign({
user: vendor,
role: 'Vendor',
resource_instance: `Order:${id}`,
tenant,
});

// Sync a deleted order with Permit
await permit.api.resourceInstances.delete(`Order:${id}`);

For more sync options, see Sync users.

Enforce permissions in the server and the UI

The app enforces permissions in two places:

  • Server middleware calls permit.check(). One permit.check() call evaluates every RBAC, ABAC, and ReBAC rule that applies to the action.
  • The Vue frontend loads the user's permissions in bulk and shows or hides UI elements. See Integrate CASL with Permit for that setup.

The middleware that protects the deliver endpoint checks whether the rider can deliver the order:

server/middleware/permissions/check-rider-eligibility.ts
// user and tenant (city) come from the request headers, orderId from the path
// One check evaluates every RBAC, ABAC, and ReBAC rule on the "deliver" action
const canRiderDeliver = await permit.check({ key: user }, 'deliver', {
type: 'Order',
key: orderId,
tenant
});

// Prevent the rider from doing the delivery if not authorised
if (!canRiderDeliver) {
return {
success: false,
message: 'You are not permitted to perform this action'
};
}

The permit-nuxt-example app enforces permissions only on the server. For the frontend half, the permit-vue-example repository on the casl branch builds the same food delivery UI with permit-fe-sdk and CASL.

In that app, src/stores/abilities.ts creates the permit-fe-sdk client with a backendUrl and calls loadLocalStateBulk() once with every action and resource the UI checks: create, read, and delete on Meal, and create, read, fulfill, assign-rider, and deliver on Order. Components then read the stored results with permit.value.check(action, resource, resourceAttributes), which returns a boolean without calling the backend again.

The deliver handler in src/components/OrdersDisplay.vue checks before it sends the request:

src/components/OrdersDisplay.vue (script setup): the deliver handler
const deliver = async (orderId: number) => {
if (permit.value.check('deliver', 'Order', {})) {
isDelivering.value = true;
const success = await orders.deliver(orderId);
isDelivering.value = false;
if (success) {
toast.add({
severity: 'success',
summary: 'Order Delivered',
life: 3000
});
}
}
};

The same check hides the button, so a rider who cannot deliver the order never sees it:

src/components/OrdersDisplay.vue (template): the Deliver button
<Button
type="submit"
label="Deliver"
v-if="!order.deliveredTime && permit.check('deliver', 'Order', {})"
@click="deliver(order.id)"
:loading="isDelivering"
/>

The Fulfill button and the assign rider form in the same component follow this pattern with the fulfill and assign-rider actions. Read src/components/OrdersDisplay.vue for the complete component.

Frontend checks hide controls, they do not enforce

permit.check() in the browser reads a result the backend already returned, and anyone can call your API directly. Keep the server middleware check on every endpoint. A UI-only check leaves the endpoint open.

Test the full policy

Run the PDP and the app, then test each rule.

  1. Start the PDP in one terminal:

    docker run -it -p 7766:7000 --env PDP_API_KEY=your-permit-api-key --env PDP_DEBUG=True permitio/pdp-v2:latest
  2. Start the app in a second terminal, from the project root:

    npm run dev
  3. Test the authorization rules. Use the sidebar to switch the current user, the current user's roles, and the current city. Each row lists the expected result:

    TestExpected resultModel that decides
    As a customer, create an orderThe order appears in the order list.RBAC
    As a customer, create an order whose meals total 500 or moreThe order's Delivery Fee row reads FREE.ABAC
    As a customer, create an order under 500The Delivery Fee row shows a fee.ABAC
    As the vendor who created the meals, fulfill the orderThe order's Fulfilled Time fills in.RBAC and ReBAC
    As a vendor who created none of the meals, fulfill the order"You are not permitted to perform this action".ReBAC
    As an admin, assign a rider to the orderThe order's Rider and Rider Assigned Time fill in.RBAC
    As the assigned rider, deliver the orderThe order's Delivered Time fills in.RBAC, ABAC, and ReBAC
    As a rider who was not assigned the order, deliver it"You are not permitted to perform this action".ReBAC
    As a user with no role in the current city, create an order"You are not permitted to perform this action".RBAC
  4. Test tenant isolation. Assign a user the customer role in california only, create an order there, then switch the city to washington and create another order. The second attempt answers "You are not permitted to perform this action", because the role assignment belongs to the california tenant.

Verify: every row above matches. When a row does not, open the Audit Log screen in Permit and select the entry for that check. The entry shows the user, action, resource, and tenant, and its decision log carries a human-readable reason for the result.

Next steps