Role Menu Permission Service — Full Architecture
⚡Intermediate · 1–3 yrsIntermediate1–3 yrs experience Builds on the foundations. Expect trade-off discussions, light math, and short code snippets.Prerequisites: Solid grasp of networking, databases, and at least one web framework.What Problem Are We Solving?
In any application with multiple backend services, every team ends up writing the same code over and over:
- "Is this user an Admin? Can they access this route?"
- "Which menu items should a mobile app show this user?"
- "Does this user have permission to create, read, update, or delete?"
Without a centralized solution, each service has its own checkRole() or hasPermission() logic, its own tables, its own bugs. Changes ripple everywhere.
The Role Menu Permission Service is a dedicated microservice that owns all of this logic — one authoritative source of truth for roles, menus, and permissions across your entire platform.
Big Picture — What This Service Does
Core idea: Clients talk REST. Backend services talk gRPC. Everything goes through one permission service.
Core Concepts — The Building Blocks
Before diving into architecture, understand the three core entities:
🏷️ Role
A named collection of permissions. Examples: super-admin, store-manager, customer, read-only-viewer.
🔑 Permission
A specific action on a resource. Modeled as resource:action:scope:
orders:create:own— create your own ordersproducts:delete:any— delete any productusers:read:any— view any user profile
📋 Menu Item
A navigation entry in a web/mobile app. It's tied to a permission — if the user lacks the permission, the menu item disappears.
Full System Architecture
Request Flow — Step by Step
Flow 1: Web/Mobile App Loads a Menu
Flow 2: Backend Service Checks a Permission via gRPC
API Design
REST API (for Web & Mobile Clients)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET | /v1/me/menus?platform=web | Get current user's menu tree | JWT |
GET | /v1/me/permissions | Get all effective permissions | JWT |
GET | /v1/roles | List all roles | Admin |
POST | /v1/roles | Create a new role | Admin |
PUT | /v1/roles/:id | Update role details | Admin |
DELETE | /v1/roles/:id | Delete a role | Admin |
POST | /v1/roles/:id/permissions | Assign permissions to role | Admin |
DELETE | /v1/roles/:id/permissions/:permId | Revoke a permission | Admin |
GET | /v1/permissions | List all permissions | Admin |
POST | /v1/permissions | Create permission entry | Admin |
GET | /v1/menus | List all menu items | Admin |
POST | /v1/menus | Create menu item | Admin |
PUT | /v1/menus/:id | Update menu item | Admin |
DELETE | /v1/menus/:id | Delete menu item | Admin |
POST | /v1/users/:id/roles | Assign role to user | Admin |
DELETE | /v1/users/:id/roles/:roleId | Remove role from user | Admin |
Sample REST Responses
GET /v1/me/menus?platform=web
{
"success": true,
"data": {
"platform": "web",
"menus": [
{
"id": "m-001",
"title": "Dashboard",
"icon": "dashboard",
"route": "/dashboard",
"sortOrder": 1,
"children": []
},
{
"id": "m-002",
"title": "Orders",
"icon": "shopping-cart",
"route": "/orders",
"sortOrder": 2,
"children": [
{
"id": "m-003",
"title": "All Orders",
"icon": "list",
"route": "/orders/list",
"sortOrder": 1,
"children": []
},
{
"id": "m-004",
"title": "Create Order",
"icon": "plus",
"route": "/orders/create",
"sortOrder": 2,
"children": []
}
]
}
]
}
}gRPC API (for Internal Backend Services)
This is the high-performance, low-latency path used by all your microservices.
syntax = "proto3";
package permissions.v1;
// ─────────────────────────────────────────────
// The Permission Service contract
// All internal services use this to check access
// ─────────────────────────────────────────────
service PermissionService {
// Check if a user has a specific permission
rpc CheckPermission(CheckPermissionRequest) returns (CheckPermissionResponse);
// Batch check multiple permissions at once
rpc CheckPermissions(CheckPermissionsRequest) returns (CheckPermissionsResponse);
// Get all permissions a user holds (for middleware caching)
rpc GetUserPermissions(GetUserPermissionsRequest) returns (GetUserPermissionsResponse);
// Validate a JWT and return user info + roles in one call
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
}
// ─── Request / Response Messages ───
message CheckPermissionRequest {
string user_id = 1;
string resource = 2; // e.g. "orders"
string action = 3; // e.g. "create", "read", "update", "delete"
string scope = 4; // e.g. "own", "any" (optional)
}
message CheckPermissionResponse {
bool allowed = 1;
string role = 2; // which role granted this
string reason = 3; // human-readable explanation
}
message CheckPermissionsRequest {
string user_id = 1;
repeated Permission permissions = 2;
}
message Permission {
string resource = 1;
string action = 2;
string scope = 3;
}
message CheckPermissionsResponse {
repeated PermissionResult results = 1;
}
message PermissionResult {
string resource = 1;
string action = 2;
bool allowed = 3;
}
message GetUserPermissionsRequest {
string user_id = 1;
}
message GetUserPermissionsResponse {
string user_id = 1;
repeated string roles = 2;
repeated string permissions = 3; // ["orders:create:any", "products:read:any"]
}
message ValidateTokenRequest {
string jwt_token = 1;
}
message ValidateTokenResponse {
bool valid = 1;
string user_id = 2;
string email = 3;
repeated string roles = 4;
repeated string permissions = 5;
int64 expires_at = 6;
}Database Schema
-- ─────────────────────────────────────────────
-- Core Tables
-- ─────────────────────────────────────────────
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE, -- e.g. 'super-admin'
description TEXT,
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- system roles can't be deleted
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
resource VARCHAR(100) NOT NULL, -- e.g. 'orders', 'products'
action VARCHAR(50) NOT NULL, -- e.g. 'create', 'read', 'update', 'delete'
scope VARCHAR(50) NOT NULL DEFAULT 'any', -- 'own' or 'any'
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(resource, action, scope)
);
CREATE TABLE menu_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR(200) NOT NULL,
icon VARCHAR(100),
route VARCHAR(500),
platform VARCHAR(20) NOT NULL CHECK (platform IN ('web', 'mobile', 'both')),
sort_order INT NOT NULL DEFAULT 0,
parent_id UUID REFERENCES menu_items(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE SET NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
granted_by UUID, -- admin user id
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE user_roles (
user_id UUID NOT NULL, -- foreign key to your User Service
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
assigned_by UUID, -- admin who made the assignment
expires_at TIMESTAMPTZ, -- optional: temporary role assignment
PRIMARY KEY (user_id, role_id)
);
-- ─────────────────────────────────────────────
-- Audit Trail
-- ─────────────────────────────────────────────
CREATE TABLE permission_audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL,
action VARCHAR(50) NOT NULL, -- 'ROLE_ASSIGNED', 'PERMISSION_CHECKED'
resource VARCHAR(100),
result VARCHAR(20), -- 'ALLOWED', 'DENIED'
ip_address INET,
user_agent TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ─────────────────────────────────────────────
-- Indexes for Performance
-- ─────────────────────────────────────────────
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
CREATE INDEX idx_menu_items_platform ON menu_items(platform, is_active, sort_order);
CREATE INDEX idx_menu_items_parent_id ON menu_items(parent_id);
CREATE INDEX idx_audit_log_user_id ON permission_audit_log(user_id, created_at);Caching Strategy
One of the most important parts of this service is caching — permission checks happen on EVERY request across every service, so they must be near-instant.
Cache Key Patterns
| Cache Key | Content | TTL |
|---|---|---|
perm:{userId}:all | All permission slugs for user | 5 min |
perm:{userId}:{resource}:{action} | Single permission check result | 5 min |
menu:{platform}:{roleIds sorted} | Full menu tree for roles | 5 min |
role:{roleId}:perms | All permissions for one role | 10 min |
user:{userId}:roles | All role IDs for a user | 5 min |
Cache Invalidation Rules
Whenever an admin changes anything via the REST API, the service instantly deletes related cache entries:
Role updated → Delete: role:{id}:perms
Delete: perm:*:{roleId}:* (all users with that role)
Permission changed → Delete: role:{roleId}:perms
Delete: perm:{userId}:all (all affected users)
User role changed → Delete: user:{userId}:roles
Delete: perm:{userId}:*
Delete: menu:*:{userId}:*Integration — How Other Services Use This
Option A: Middleware Approach (Recommended)
Each service runs a thin permission middleware that calls the Permission Service via gRPC on every incoming request. The result is attached to the request context.
// permission-middleware.js
// Drop this into any Express/Fastify service
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PERM_SERVICE_ADDR =
process.env.PERMISSION_SERVICE_GRPC_ADDR || "permission-svc:50051";
const packageDef = protoLoader.loadSync("./permission.proto", {
keepCase: true,
longs: String,
enums: String,
});
const { PermissionService } =
grpc.loadPackageDefinition(packageDef)["permissions.v1"];
const client = new PermissionService(
PERM_SERVICE_ADDR,
grpc.credentials.createInsecure()
);
/**
* Factory: create a middleware that checks a specific permission
*
* Usage: router.post('/orders', requirePermission('orders', 'create'), handler)
*/
function requirePermission(resource, action, scope = "any") {
return (req, res, next) => {
const userId = req.user?.id; // set by your JWT middleware
if (!userId) return res.status(401).json({ error: "Unauthenticated" });
client.CheckPermission(
{ user_id: userId, resource, action, scope },
(err, response) => {
if (err) {
console.error("Permission service unavailable:", err.message);
return res
.status(503)
.json({ error: "Authorization service unavailable" });
}
if (!response.allowed) {
return res.status(403).json({
error: "Forbidden",
reason: response.reason,
});
}
req.permission = response; // available in route handler
next();
}
);
};
}
module.exports = { requirePermission };Using it in any service:
// orders-service/routes/orders.js
const { requirePermission } = require("./permission-middleware");
router.get("/orders", requirePermission("orders", "read"), async (req, res) => {
const orders = await OrderModel.findAll();
res.json({ orders });
});
router.post(
"/orders",
requirePermission("orders", "create"),
async (req, res) => {
const order = await OrderModel.create(req.body);
res.json({ order });
}
);
router.delete(
"/orders/:id",
requirePermission("orders", "delete", "any"),
async (req, res) => {
await OrderModel.deleteById(req.params.id);
res.json({ success: true });
}
);Option B: SDK Approach
Publish a shared npm package @yourorg/permission-sdk that any Node.js service can install:
npm install @yourorg/permission-sdk// Usage in any service
const { PermissionClient } = require("@yourorg/permission-sdk");
const perms = new PermissionClient({
serviceAddr: process.env.PERMISSION_SERVICE_ADDR,
// Enables local in-memory cache for 30s to reduce gRPC calls
localCacheTtl: 30_000,
});
// One-liner check
const allowed = await perms.can(userId, "orders:create:any");
// Middleware factory
app.post("/products", perms.require("products:create"), handler);
// Batch check
const results = await perms.checkMany(userId, [
"orders:read",
"products:update",
"users:delete",
]);
// { 'orders:read': true, 'products:update': false, 'users:delete': false }Mobile & Web Frontend Integration
How a Web App Uses the Menu API
// React hook: useNavMenu.js
import { useEffect, useState } from "react";
import axios from "axios";
export function useNavMenu() {
const [menus, setMenus] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios
.get("/api/v1/me/menus?platform=web", {
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
})
.then((res) => setMenus(res.data.data.menus))
.catch(console.error)
.finally(() => setLoading(false));
}, []);
return { menus, loading };
}
// NavBar.jsx
function NavBar() {
const { menus, loading } = useNavMenu();
if (loading) return <Skeleton />;
return (
<nav>
{menus.map((item) => (
<NavItem key={item.id} item={item} />
))}
</nav>
);
}How a Mobile App Uses the Menu API
// iOS — Swift example
struct MenuView: View {
@State private var menus: [MenuItem] = []
var body: some View {
List(menus, id: \.id) { item in
NavigationLink(destination: RouteView(route: item.route)) {
Label(item.title, systemImage: item.icon)
}
}
.onAppear { fetchMenus() }
}
func fetchMenus() {
let url = URL(string: "\(API_BASE)/v1/me/menus?platform=mobile")!
var request = URLRequest(url: url)
request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, _, _ in
if let data = data,
let response = try? JSONDecoder().decode(MenuResponse.self, from: data) {
DispatchQueue.main.async { self.menus = response.data.menus }
}
}.resume()
}
}Security Architecture
Security Rules
| Rule | Why |
|---|---|
| JWT secret is only known to Auth Service and API Gateway | Permission Service never issues tokens — it only evaluates permissions |
| gRPC endpoint is not exposed externally | Only internal Kubernetes services can reach port 50051 |
| System roles cannot be deleted | is_system = true roles like super-admin are protected |
| All admin actions are audit-logged | You can trace who changed what permission and when |
| Cache TTL is max 5 minutes | Role changes propagate within 5 minutes without manual intervention |
| Permission check failures are safe-fail | If the service is down, requests are denied (fail-closed), not allowed |
High Availability & Scalability
Deployment Checklist
- ✅ Horizontal scaling: Run 3+ pods behind a load balancer
- ✅ PostgreSQL read replicas: All SELECT queries hit replicas; writes go to primary
- ✅ Redis Cluster: High-availability caching with automatic failover
- ✅ Health checks:
/health/liveand/health/readyendpoints - ✅ Graceful shutdown: Drain in-flight requests before pod termination
- ✅ Circuit breaker: If Redis is unavailable, fall back to DB; if DB is down, deny all
- ✅ Rate limiting: Admin API is rate-limited to prevent abuse
Observability
Key Metrics to Monitor
| Metric | Alert Threshold |
|---|---|
perm_check_latency_p99 | > 50ms |
perm_check_denied_rate | Spike in denials (possible attack) |
cache_hit_rate | < 80% (cache might be invalid) |
grpc_error_rate | > 1% |
menu_api_response_time | > 200ms |
db_query_time_p99 | > 100ms |
Environment Variables & Configuration
# ─── Service Identity ───
SERVICE_NAME=role-menu-permission-service
SERVICE_VERSION=1.0.0
# ─── REST API ───
PORT=3000
NODE_ENV=production
# ─── gRPC ───
GRPC_PORT=50051
GRPC_MAX_CONNECTIONS=100
# ─── PostgreSQL ───
DB_HOST=postgres-primary
DB_PORT=5432
DB_NAME=permissions
DB_USER=perm_svc_user
DB_PASSWORD=<secret>
DB_REPLICA_HOST=postgres-replica
# ─── Redis ───
REDIS_HOST=redis-cluster
REDIS_PORT=6379
REDIS_PASSWORD=<secret>
CACHE_DEFAULT_TTL=300
# ─── JWT ───
JWT_PUBLIC_KEY=<rsa-public-key> # Only needed to verify tokens
# Never store the private key here
# ─── Security ───
ADMIN_API_RATE_LIMIT=100 # requests per minute
GRPC_TLS_ENABLED=true
# ─── Observability ───
LOG_LEVEL=info
JAEGER_ENDPOINT=http://jaeger:14268/api/traces
PROMETHEUS_PORT=9090Project Folder Structure
role-menu-permission-service/
├── src/
│ ├── api/
│ │ ├── rest/
│ │ │ ├── routes/
│ │ │ │ ├── roles.routes.js # POST/GET/DELETE /roles
│ │ │ │ ├── permissions.routes.js # CRUD /permissions
│ │ │ │ ├── menus.routes.js # CRUD /menus
│ │ │ │ └── me.routes.js # GET /me/menus, /me/permissions
│ │ │ ├── middleware/
│ │ │ │ ├── auth.middleware.js # JWT verification
│ │ │ │ ├── require-admin.js # Admin role gate
│ │ │ │ └── validate.js # Request body validation
│ │ │ └── app.js # Express/Fastify setup
│ │ │
│ │ └── grpc/
│ │ ├── server.js # gRPC server bootstrap
│ │ ├── handlers/
│ │ │ ├── checkPermission.js # CheckPermission RPC
│ │ │ ├── checkPermissions.js # Batch check RPC
│ │ │ ├── getUserPermissions.js # GetUserPermissions RPC
│ │ │ └── validateToken.js # ValidateToken RPC
│ │ └── proto/
│ │ └── permission.proto # gRPC contract file
│ │
│ ├── services/
│ │ ├── role.service.js # Role CRUD business logic
│ │ ├── permission.service.js # Permission evaluation engine
│ │ └── menu.service.js # Dynamic menu tree builder
│ │
│ ├── repositories/
│ │ ├── role.repository.js # DB queries for roles
│ │ ├── permission.repository.js # DB queries for permissions
│ │ ├── menu.repository.js # DB queries for menu items
│ │ └── audit.repository.js # Audit log writes
│ │
│ ├── cache/
│ │ ├── redis.client.js # Redis connection + helpers
│ │ ├── keys.js # Cache key templates
│ │ └── invalidator.js # Cache invalidation logic
│ │
│ ├── db/
│ │ ├── connection.js # PostgreSQL pool setup
│ │ └── migrations/ # SQL migration files
│ │
│ ├── config/
│ │ └── env.js # Validated env config
│ │
│ └── index.js # Entry point: start REST + gRPC
│
├── sdk/ # Publishable client SDK
│ ├── index.js
│ ├── permission-client.js
│ └── middleware.js
│
├── proto/
│ └── permission.proto # Shared proto file
│
├── tests/
│ ├── unit/
│ └── integration/
│
├── docker-compose.yml
├── Dockerfile
├── package.json
└── README.mdSummary — Decision Map
Quick Reference
| Topic | Decision |
|---|---|
| Protocol: clients | REST over HTTPS |
| Protocol: services | gRPC (internal only, not exposed) |
| Caching | Redis, 5 min TTL, invalidate on change |
| Database | PostgreSQL (primary + read replica) |
| Auth model | RBAC — Roles → Permissions → Resources |
| Menu model | Hierarchical tree, filtered by user roles |
| Integration | Drop-in middleware OR SDK npm package |
| Fail behavior | Fail-closed — deny if permission service is down |
| Scalability | Horizontal pod scaling, Redis cluster |
| Audit | Full immutable audit log of all access decisions |
