Architecture

Access control

Complete control of your application's features and data is a key aspect of Backstack.


Overview

Access control should be thought of as "does the app version provide this feature? And if it does, what permissions does the user have to access it?"

All aspects of access control are defined using the Backstack dashboard.


Enforcing access control

The session.access object holds all feature permissions available fot the current user. It combines multiple sources:

  • Base features defined in the application's domain model and version schema
  • Permission settings inherited from all roles assigned to the user
{
   ...
   "access": {
      "account-users": "crud",
      ...
   }
}

The access key is the feature ID, and the value is a string of CRUD permissions.

Example access control function

You create a function in your preferred language to enforce access control against the session.access values. Your function should return a boolean result. Here's an example in JavaScript:

export function hasAccess(requiredAccess, sessionAccess) {
   // Check if the requiredAccess is "*"
   if (requiredAccess === "*") {
      return true; // Grant access to all users
   }
   
   if (requiredAccess?.length > 0 && sessionAccess) {

      // Split the control string into individual features and their permissions
      const controlList = requiredAccess.split(',');

      // Iterate through each feature in the control string
      for (const control of controlList) {

         // requiredAccess could be "*,*,*" (e.g. combining '*' constants)
         if (control === "*") {
            return true; // Grant access to all users
         }

         // Split each feature and its permissions. If no permissions assume any.
         const [feature, permissions = "*"] = control.split(':');

         // Check if the feature exists in the sessionAccess object
         if (sessionAccess.hasOwnProperty(feature)) {
            // If permissions is "*", consider it as a wildcard and return true
            if (permissions === "*") {
               return true;
            }

            // Check if the user's permissions include any of the required permissions
            for (const permission of permissions) {
               if (sessionAccess[feature].includes(permission)) {
                  // If any permission is granted, return true
                  return true;
               }
            }
         }
      }
   }
   
   // If none of the features have been found or none of the permissions match, return false
   return false;
}

Every page should begin with an access control check. This enforces the app domain and versioning schemas.

if(!hasAccess('account-users', session?.access)){
    notFound()
}

Tip

Use your routing system or middleware to run application access checks before routing to the page.

Once you enforce the application schema, you can add permission-based feature control checks.

if(hasAccess('account-users:d')){
    <button>Delete User</button>
}

Feature IDs

We recommend using kebab-case for feature IDs.

Format: {subject-name}-{feature-name}

Examples:

  • account-users (account subject)
  • app-optional-features (app subject)
  • payment-gateway-tokens (payment gateway subject)

The :{permissions} suffix will be added by the API based on the user roles. Options are:

  • c - Create
  • r - Read
  • u - Update
  • d - Delete

Then, in your hasAccess() function, you would pass in account-users:cru to check if the user has permission to create, read, or update the user.

Here are some examples:

// Is a page accessable?
if(!hasAccess('account-users', session?.access)){
    notFound()
}

// Is the page read only? (after the above check)
const readOnly = !hasAccess('account-users:cud', session?.access)

// Is a sidebar section accessible?
if(hasAccess('account-settings,account-payment-methods,account-users,account-invoices', session?.access)) {
    <AccountSidebarOptions>
    // Is a sidebar option accessable?
    if (hasAccess('account-payment-methods'))
        <PaymentMethods/>
    }
    </AccountSidebarOptions>
}

// Can the user delete a record?
if(hasAccess('account-users:d', session?.access)){
   <button>Delete User</button>
}

Why this convention?

Kebab-case feature IDs create clearer, more searchable code while reducing errors through visual separation. This format aligns well with REST APIs and common permission systems, making it instantly familiar to other developers.


Cross-cutting features

When defining features, developers need to balance between specific domain needs and reusable functionality. The key is to identify truly shared capabilities while keeping domain-specific features separate.

Consider these aspects when deciding between generic and specific features:

  • Feature titles and descriptions are included in monetization results such as pricing and feature matrices
  • Roles and permissions are assigned to every feature
  • Features can be optionally enabled or disabled
  • Developers must be able to easily apply them to blocks of related code

Here are some examples:

Generic features

  • user-settings
  • account-users
  • account-payment-settings
  • app-signup
  • image-gallaries
  • payment-settings

Domain-specific features

  • insurance-claims
  • lab-results
  • menu-items
  • test-builder

Define as many as you need to make access control as simple as possible.

Previous
Sessions