Authorisation – what an authenticated user is allowed to do. Authentication asks ‘who are you?’. Authorisation asks ‘can you do this?’. Login alone does not mean someone can delete every record.
Roles in the database
A simple role column on the users table is enough to start: user, editor, admin.
ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user';
Check permissions in one place
<?php
declare(strict_types=1);
namespace AppServices;
class Authorizer
{
private const PERMISSIONS = [
'admin' => ['notes.create', 'notes.delete', 'users.manage'],
'editor' => ['notes.create', 'notes.delete'],
'user' => ['notes.create'],
];
public function can(string $role, string $permission): bool
{
return in_array($permission, self::PERMISSIONS[$role] ?? [], true);
}
}
Gate controller actions
public function destroy(int $id): void
{
requireAuth();
$user = $this->users->findById(currentUserId());
if (!$this->authorizer->can($user['role'], 'notes.delete')) {
http_response_code(403);
echo 'Forbidden';
return;
}
$note = $this->notes->findById($id);
if (!$note || (int) $note['user_id'] !== (int) $user['id'] && $user['role'] !== 'admin') {
http_response_code(404);
return;
}
$this->notes->delete($id);
// redirect with flash
}
Notice the ownership check: editors delete their own notes; admins can delete any. Authorisation is often about both role and resource ownership.
Do not hide buttons only
Hiding a delete link in HTML is not security. Always check on the server when the POST or DELETE hits your app. Attackers can send requests directly.
403 vs 404
Return 403 when the user is logged in but not allowed. Some apps return 404 instead to avoid leaking whether a resource exists. Pick one approach and stay consistent.
Start with a role column and a small permissions map. Refactor to a permissions table only when you actually need it.

