Node.js 54 🟢 Role-Based (RBAC) and Attribute-Based (ABAC) Access Control
Authentication answers who the user is. Authorization answers what the user may do. The two are separate concerns, and conflating them produces systems where permissions are scattered across route handlers, hidden in conditionals, and impossible to audit. Two models dominate the field. Role-Based Access Control assigns permissions to roles and roles to users; it is simple, auditable, and the right choice for most applications. Attribute-Based Access Control evaluates policies against a combination of user, resource, action, and environment attributes; it is more expressive and the right choice when the rules cannot be expressed as a fixed set of roles. Most real systems start with RBAC and reach for ABAC only when a specific requirement forces it.
Key point: Authorization is a policy decision, and policy decisions should be centralized, not scattered across handlers. A permission check inside a route is a policy that is invisible to auditors, invisible to tests, and easy to forget. A centralized policy function or middleware is the same decision, made once, in one place.
Why authorization needs its own model
The scattered-check problem. When permission logic lives inside each handler, the same rule is written multiple times, in slightly different forms. A change to the rule requires finding every copy. A missing check on one route is a security hole that no test may catch.
The role explosion problem. A naive RBAC implementation creates a role for every combination of permissions. editor_can_publish, editor_can_delete, editor_can_approve becomes dozens of roles. The model breaks down because roles multiply faster than the code.
The context problem. Some decisions depend on more than the user’s role. A user may edit their own posts but not others’. A manager may view salaries for their direct reports but not for the whole company. A user may access a resource only during business hours. RBAC cannot express these rules because they depend on the resource and the environment, not just the role.
The audit problem. A compliance review asks: who can delete a customer record? If the answer is spread across twelve handlers, each with its own check, the review becomes a manual search. A centralized policy answers the question in one place.
The testing problem. Permission logic is hard to test when it is embedded in handlers. A centralized policy function is a pure function that takes a request context and returns a decision. It can be tested exhaustively without spinning up an HTTP server.
The performance problem. Permissions may need to be fetched from a database on every request. Caching the user’s role and permissions in the session or in a short-lived cache avoids the lookup. The model determines what needs to be cached.
a. Role-Based Access Control
RBAC assigns permissions to roles and roles to users. The user’s effective permissions are the union of the permissions granted by every role they hold.
The data model
The canonical schema has five tables. Users hold roles through a join table, and roles hold permissions through another join table.
users (id, email, password_hash)
roles (id, name)
permissions (id, name, description)
user_roles (user_id, role_id)
role_permissions (role_id, permission_id)
The join tables are what make roles reusable. A role is a named bundle of permissions, and a user can hold multiple roles.
The permission check
A route that requires a specific permission checks the user’s effective permission set.
function requirePermission(permission) {
return async (req, res, next) => {
if (!req.user) return res.status(401).json({ error: "unauthenticated" });
const permissions = await getUserPermissions(req.user.id);
if (!permissions.has(permission)) {
return res.status(403).json({ error: "forbidden" });
}
next();
};
}
app.delete("/posts/:id",
requireAuth,
requirePermission("post:delete"),
deletePostHandler);
The middleware is composed with the authentication middleware. The route handler runs only if the user has the permission.
The role hierarchy
A hierarchy lets roles inherit permissions from other roles. An admin role inherits everything from editor, which inherits from viewer. The schema adds a parent_id column to the roles table.
roles (id, name, parent_id)
The permission lookup walks the hierarchy and unions the permissions of all ancestors.
async function getUserPermissions(userId) {
const roles = await getRolesWithAncestors(userId);
const permissions = new Set();
for (const role of roles) {
for (const perm of role.permissions) {
permissions.add(perm.name);
}
}
return permissions;
}
The hierarchy reduces role proliferation because common permissions live in the base roles.
Caching the permission set
The permission lookup is on the hot path of every request. Caching the effective permission set in the session or in a short-lived cache avoids the database round trip.
async function getUserPermissions(userId) {
const cached = await cache.get(`perms:${userId}`);
if (cached) return new Set(cached);
const permissions = await loadPermissionsFromDb(userId);
await cache.set(`perms:${userId}`, [...permissions], { ttl: 300 });
return permissions;
}
The cache must be invalidated when the user’s roles change. A short TTL bounds the staleness.
The role assignment
Roles are assigned by administrators, not by the user. The assignment is a privileged operation and requires its own permission check.
app.post("/admin/users/:id/roles",
requireAuth,
requirePermission("user:assign_role"),
async (req, res) => {
await assignRole(req.params.id, req.body.roleId);
await cache.del(`perms:${req.params.id}`);
res.json({ ok: true });
});
The cache invalidation is part of the assignment. A stale cache after a role change is a security bug.
b. Attribute-Based Access Control
ABAC evaluates policies against four categories of attributes: subject (the user), resource (the object being accessed), action (what is being done), and environment (the context of the request). The policy is a function that returns a decision.
The policy function
The policy is a pure function. It takes the request context and returns a boolean. The context includes the user, the resource, the action, and any environment data.
function canEditPost(user, post, env) {
if (user.role === "admin") return true;
if (post.authorId === user.id) return true;
if (user.role === "moderator" && post.status === "flagged") return true;
if (env.time.getHours() < 9 || env.time.getHours() > 17) return false;
return false;
}
The function is testable in isolation. The rules are visible in one place. The context is passed explicitly, so nothing is hidden in the request object.
The policy engine
A policy engine stores rules as data and evaluates them at runtime. The rules can be expressed in a DSL, in JSON, or in a small expression language.
const policies = [
{
action: "post:edit",
effect: "allow",
condition: (ctx) => ctx.user.id === ctx.resource.authorId,
},
{
action: "post:edit",
effect: "allow",
condition: (ctx) => ctx.user.role === "admin",
},
{
action: "post:edit",
effect: "deny",
condition: (ctx) => ctx.resource.locked === true,
},
];
function evaluate(action, context) {
const applicable = policies.filter((p) => p.action === action);
for (const policy of applicable) {
if (policy.effect === "deny" && policy.condition(context)) return false;
}
for (const policy of applicable) {
if (policy.effect === "allow" && policy.condition(context)) return true;
}
return false;
}
The deny rules are evaluated first, so an explicit deny overrides any allow. The default is deny.
The policy decision point
The policy engine is the Policy Decision Point (PDP). The route handler is the Policy Enforcement Point (PEP). The handler asks the PDP for a decision and enforces it.
async function authorize(action, resourceLoader) {
return async (req, res, next) => {
const resource = await resourceLoader(req);
const context = {
user: req.user,
resource,
action,
env: { time: new Date(), ip: req.ip },
};
if (!evaluate(action, context)) {
return res.status(403).json({ error: "forbidden" });
}
req.resource = resource;
next();
};
}
app.patch("/posts/:id",
requireAuth,
authorize("post:edit", (req) => Post.findByPk(req.params.id)),
updatePostHandler);
The separation is the point. The policy engine knows nothing about HTTP. The route handler knows nothing about policy. Each can be tested and changed independently.
The policy externalization
For larger systems, the policy can live outside the application. OPA (Open Policy Agent) is a general-purpose policy engine that evaluates Rego policies. The application sends a query with the context, and OPA returns a decision.
const response = await fetch("http://opa:8181/v1/data/posts/edit/allow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: context }),
});
const { result } = await response.json();
if (!result) return res.status(403).end();
The policy is versioned separately, reviewed by security, and applied uniformly across services.
c. Choosing between RBAC and ABAC
The two models are not mutually exclusive. Most systems use RBAC for the coarse-grained decisions and ABAC for the rules that depend on the resource or the environment.
When RBAC is sufficient
RBAC is sufficient when the permissions are stable, the roles are few, and the rules do not depend on the resource’s properties. An admin panel with a fixed set of features is a good fit. A content management system with editors, authors, and reviewers is a good fit.
When ABAC is required
ABAC is required when the rules depend on the resource’s owner, the resource’s state, the time of day, the user’s department, or any other attribute that varies per request. A document-sharing system where access depends on the document’s sharing settings is a good fit. A healthcare system where access depends on the patient-provider relationship is a good fit.
The hybrid pattern
The hybrid pattern uses roles to gate the entry point and attributes to gate the specific resource. The role check is cheap and runs first. The attribute check runs only if the role check passes.
app.patch("/posts/:id",
requireAuth,
requirePermission("post:edit"),
authorize("post:edit", (req) => Post.findByPk(req.params.id)),
updatePostHandler);
The permission check confirms the user has the capability at all. The attribute check confirms the user has the capability for this specific resource.
The decision table
| Scenario | Model |
|---|---|
| Fixed feature set, few roles | RBAC |
| Resource ownership | ABAC |
| Time-based access | ABAC |
| Department-based access | ABAC |
| Coarse-grained gating | RBAC |
| Fine-grained resource rules | ABAC |
| Both coarse and fine | Hybrid |
The anti-pattern: roles for everything
A common mistake is to model every attribute as a role. post_owner, post_moderator, post_admin becomes dozens of pseudo-roles. The role table becomes a denormalized copy of the resource’s state. This is a sign that ABAC is needed.
Complete Example Session
// ============================================
// PART 1: RBAC DATA MODEL
// ============================================
// users (id, email, password_hash)
// roles (id, name)
// permissions (id, name)
// user_roles (user_id, role_id)
// role_permissions (role_id, permission_id)
async function getUserPermissions(userId) {
const rows = await db.query(`
SELECT DISTINCT p.name
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
`, [userId]);
return new Set(rows.map((r) => r.name));
}
// ============================================
// PART 2: RBAC MIDDLEWARE
// ============================================
function requirePermission(permission) {
return async (req, res, next) => {
if (!req.user) return res.status(401).json({ error: "unauthenticated" });
const permissions = await getUserPermissions(req.user.id);
if (!permissions.has(permission)) {
return res.status(403).json({ error: "forbidden" });
}
next();
};
}
// ============================================
// PART 3: THE ABAC POLICY ENGINE
// ============================================
const policies = [
{
action: "post:edit",
effect: "deny",
condition: (ctx) => ctx.resource.locked,
},
{
action: "post:edit",
effect: "allow",
condition: (ctx) => ctx.user.role === "admin",
},
{
action: "post:edit",
effect: "allow",
condition: (ctx) => ctx.user.id === ctx.resource.authorId,
},
];
function evaluate(action, context) {
const applicable = policies.filter((p) => p.action === action);
for (const p of applicable) {
if (p.effect === "deny" && p.condition(context)) return false;
}
for (const p of applicable) {
if (p.effect === "allow" && p.condition(context)) return true;
}
return false;
}
// ============================================
// PART 4: THE ABAC MIDDLEWARE
// ============================================
function authorize(action, loadResource) {
return async (req, res, next) => {
const resource = await loadResource(req);
if (!resource) return res.status(404).json({ error: "not found" });
const context = {
user: req.user,
resource,
action,
env: { time: new Date(), ip: req.ip },
};
if (!evaluate(action, context)) {
return res.status(403).json({ error: "forbidden" });
}
req.resource = resource;
next();
};
}
// ============================================
// PART 5: THE HYBRID ROUTE
// ============================================
app.patch("/posts/:id",
requireAuth,
requirePermission("post:edit"),
authorize("post:edit", (req) => Post.findByPk(req.params.id)),
async (req, res) => {
await req.resource.update(req.body);
res.json(req.resource);
});
// ============================================
// PART 6: POLICY TESTS
// ============================================
const assert = require("node:assert");
const admin = { id: 1, role: "admin" };
const author = { id: 2, role: "author" };
const other = { id: 3, role: "author" };
const post = { id: 10, authorId: 2, locked: false };
const lockedPost = { id: 11, authorId: 2, locked: true };
assert.strictEqual(evaluate("post:edit", { user: admin, resource: post }), true);
assert.strictEqual(evaluate("post:edit", { user: author, resource: post }), true);
assert.strictEqual(evaluate("post:edit", { user: other, resource: post }), false);
assert.strictEqual(evaluate("post:edit", { user: admin, resource: lockedPost }), false);
assert.strictEqual(evaluate("post:edit", { user: author, resource: lockedPost }), false);
// ============================================
// PART 7: CACHING PERMISSIONS
// ============================================
async function getUserPermissionsCached(userId) {
const key = `perms:${userId}`;
const cached = await redis.get(key);
if (cached) return new Set(JSON.parse(cached));
const permissions = await getUserPermissions(userId);
await redis.set(key, JSON.stringify([...permissions]), "EX", 300);
return permissions;
}
// ============================================
// PART 8: INVALIDATING ON ROLE CHANGE
// ============================================
app.post("/admin/users/:id/roles",
requireAuth,
requirePermission("user:assign_role"),
async (req, res) => {
await assignRole(req.params.id, req.body.roleId);
await redis.del(`perms:${req.params.id}`);
res.json({ ok: true });
});
// ============================================
// PART 9: THE DENY-OVERRIDES PATTERN
// ============================================
// Deny rules are evaluated first.
// An explicit deny cannot be overridden by an allow.
// This is the standard behavior in policy engines.
const policiesWithDeny = [
{ action: "post:edit", effect: "deny", condition: (ctx) => ctx.resource.locked },
{ action: "post:edit", effect: "allow", condition: (ctx) => ctx.user.role === "admin" },
];
// An admin cannot edit a locked post.
// The deny rule wins.
// ============================================
// PART 10: AUDIT LOGGING
// ============================================
function authorizeWithAudit(action, loadResource) {
return async (req, res, next) => {
const resource = await loadResource(req);
const context = { user: req.user, resource, action };
const decision = evaluate(action, context);
await auditLog.write({
userId: req.user?.id,
action,
resourceId: resource?.id,
decision: decision ? "allow" : "deny",
timestamp: new Date(),
});
if (!decision) return res.status(403).json({ error: "forbidden" });
req.resource = resource;
next();
};
}
The ten parts showed the RBAC data model, middleware, the ABAC policy engine, ABAC middleware, the hybrid route, policy tests, caching, invalidation, deny precedence, and audit logging. The same posts resource ran through every part, showing how the two models combine.
Quick Reference
RBAC Tables
| Table | Purpose |
|---|---|
users | User accounts |
roles | Named permission bundles |
permissions | Individual capabilities |
user_roles | User-to-role assignments |
role_permissions | Role-to-permission assignments |
ABAC Context
| Category | Examples |
|---|---|
| Subject | User ID, role, department, clearance |
| Resource | Owner, status, classification, locked |
| Action | Read, write, delete, approve |
| Environment | Time, IP, device, location |
RBAC vs ABAC
| Aspect | RBAC | ABAC |
|---|---|---|
| Granularity | Coarse | Fine |
| Rules | Fixed | Dynamic |
| Resource-aware | No | Yes |
| Environment-aware | No | Yes |
| Complexity | Low | High |
| Testability | Easy | Easy with pure functions |
Policy Patterns
| Pattern | Purpose |
|---|---|
| Deny first | Explicit deny overrides any allow |
| Default deny | Absence of an allow is a deny |
| Centralized PDP | Policy decisions in one place |
| PEP at route | Enforcement at the handler |
| Audit log | Record every decision |
Best Practices
✅ Do This:
// Centralize policy decisions in one function.
function evaluate(action, context) { /* ... */ } // ✅
// Use middleware for enforcement.
app.delete("/posts/:id", requireAuth, requirePermission("post:delete")); // ✅
// Deny first, then allow.
for (const p of policies) if (p.effect === "deny" && p.condition(ctx)) return false; // ✅
// Cache permissions with a short TTL.
await redis.set(key, data, "EX", 300); // ✅
// Invalidate the cache on role change.
await redis.del(`perms:${userId}`); // ✅
// Test the policy function in isolation.
assert.strictEqual(evaluate("post:edit", ctx), true); // ✅
// Log every decision.
await auditLog.write({ userId, action, decision }); // ✅
❌ Don’t Do This:
// Don't scatter permission checks across handlers.
if (req.user.role === "admin") { /* ... */ } // ❌ In every handler
// Don't trust the client's role claim.
const role = req.body.role; // ❌ Server decides
// Don't model resource attributes as roles.
role: "post_owner_42" // ❌ Role explosion
// Don't forget to invalidate the cache.
await assignRole(userId, roleId); // ❌ Stale permissions
// Don't default to allow.
return true; // ❌ Default deny instead
// Don't skip the resource load.
authorize("post:edit"); // ❌ Needs the resource for ABAC
// Don't log only denials.
// Log allows too, for audit. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Scattered checks | No centralized policy | Use a PDP function |
| Role explosion | Attributes modeled as roles | Switch to ABAC |
| Stale permissions | Cache not invalidated | Delete cache on role change |
| Missing resource check | RBAC only | Add ABAC for resource-specific rules |
| Default allow | No fallback | Default deny |
| Deny overridden | Allow evaluated first | Evaluate deny rules first |
| Inconsistent decisions | Logic duplicated | One policy engine |
| No audit trail | No logging | Log every decision |
Real-World Examples
1. RBAC Middleware
app.delete("/posts/:id", requireAuth, requirePermission("post:delete"), handler)
2. ABAC Policy
condition: (ctx) => ctx.user.id === ctx.resource.authorId
3. Hybrid Route
app.patch("/posts/:id", requireAuth, requirePermission("post:edit"), authorize("post:edit", load), handler)
4. Deny Precedence
for (const p of policies) if (p.effect === "deny" && p.condition(ctx)) return false
5. Permission Cache
await redis.set(`perms:${userId}`, data, "EX", 300)
6. Cache Invalidation
await redis.del(`perms:${userId}`)
7. Policy Test
assert.strictEqual(evaluate("post:edit", { user: author, resource: post }), true)
8. Audit Log
await auditLog.write({ userId, action, resourceId, decision })
9. Role Hierarchy
roles (id, name, parent_id)
10. External Policy Engine
const response = await fetch("http://opa:8181/v1/data/posts/edit/allow", { ... })
Visual
RBAC Structure
┌────────────────────────────────────────────────────────────┐
│ RBAC MODEL │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ users │───N:M──▶│ roles │───N:M──▶│ permissions │ │
│ └─────────┘ └─────────┘ └─────────────┘ │
│ │
│ Alice ──▶ [editor, reviewer] │
│ editor ──▶ [post:create, post:edit] │
│ reviewer ──▶ [post:approve] │
│ │
│ Alice's effective permissions: │
│ post:create, post:edit, post:approve │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Roles are named bundles of permissions. │ │
│ │ Users hold roles. │ │
│ │ The union of permissions is the effective set. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
ABAC Context
┌────────────────────────────────────────────────────────────┐
│ ABAC DECISION CONTEXT │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SUBJECT (who) │ │
│ │ user.id, user.role, user.department │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RESOURCE (what) │ │
│ │ post.authorId, post.locked, post.status │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ACTION (how) │ │
│ │ post:edit, post:delete, post:approve │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ENVIRONMENT (when/where) │ │
│ │ time, ip, device, location │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ POLICY ENGINE → allow or deny │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Hybrid Pattern
┌────────────────────────────────────────────────────────────┐
│ HYBRID RBAC + ABAC │
│ │
│ REQUEST │
│ ┌──────────────────────────────────────────────────┐ │
│ │ PATCH /posts/42 │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ requireAuth ← who is the user? │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ requirePermission("post:edit") ← RBAC │ │
│ │ Does the user have the capability at all? │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ authorize("post:edit", loadPost) ← ABAC │ │
│ │ Does the user have the capability for this │ │
│ │ specific resource? │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ HANDLER │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ RBAC gates the entry point. │
│ ABAC gates the specific resource. │
└────────────────────────────────────────────────────────────┘
Deny Precedence
┌────────────────────────────────────────────────────────────┐
│ POLICY EVALUATION ORDER │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Collect policies matching the action. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 2. Evaluate DENY rules first. │ │
│ │ If any matches → return deny. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 3. Evaluate ALLOW rules. │ │
│ │ If any matches → return allow. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 4. No match → default deny. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ An explicit deny cannot be overridden by an allow. │
│ The default is deny. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| RBAC | Permissions assigned to roles, roles to users |
| ABAC | Policies evaluated against subject, resource, action, environment |
| Effective permissions | Union of all permissions from all roles |
| Role hierarchy | Roles inherit from parent roles |
| Policy engine | Centralized decision function (PDP) |
| Enforcement point | Middleware at the route (PEP) |
| Deny precedence | Deny rules override allow rules |
| Default deny | No matching allow means denied |
| Cache invalidation | Delete on role change |
| Audit log | Record every decision |
Key takeaways:
- RBAC is the default. It is simple, auditable, and sufficient when the permissions are stable and the roles are few. The five-table schema is the canonical model.
- ABAC is for rules that depend on the resource or the environment. Resource ownership, time of day, department, and resource state cannot be expressed in RBAC without role explosion.
- The hybrid pattern is the practical choice. Use RBAC to gate the entry point and ABAC to gate the specific resource. The role check is cheap and runs first; the attribute check runs only if the role check passes.
- Centralize policy decisions. A pure function that takes a context and returns a decision is testable, auditable, and visible. Scattered checks are invisible, untested, and easy to forget.
- Deny rules override allow rules. Explicit denies are evaluated first. The default is deny. An allow rule cannot override a deny rule.
- Cache permissions with a short TTL, and invalidate on role change. The permission lookup is on the hot path. The cache must be deleted when the user’s roles change.
- Log every decision. An audit log that records allows and denies is the evidence that compliance reviews ask for. Denials alone are not enough.
- Separate the PDP from the PEP. The policy engine knows nothing about HTTP. The route handler knows nothing about policy. Each can be tested and changed independently.
Remember: Authorization is a policy decision, and policy decisions belong in one place. RBAC assigns permissions to roles and roles to users; it is simple and auditable, and it is the correct starting point for most applications. ABAC evaluates policies against the subject, resource, action, and environment; it is more expressive and the correct choice when the rules depend on the resource or the context. Real systems use both: RBAC for the coarse-grained gate, ABAC for the fine-grained resource rule. The policy engine is a pure function, tested in isolation. The middleware is the enforcement point, composed with authentication. Deny rules override allow rules, and the default is deny. Permissions are cached and invalidated on change. Every decision is logged. The result is an authorization system that is correct, testable, and auditable, and that does not hide its policy in the corners of route handlers.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!