How to Ban and Unban Users in Your App, and Make the Ban Stick [Full Repo]

Blocking the login is the easy half. A ban that does not end the sessions already open, or that the banned person can undo, is not a ban.

The _User table after this guide: three accounts with an isBanned column reading True for samir_h, undefined for dana_w and False for alex_k

Someone is abusing your app and you need them gone. Not warned, not rate-limited — gone, today, without deleting their account or waiting for a deploy.

The usual answer is a flag on the user record and a check when they log in, and it is the right shape. It is also, on its own, about half of a ban: the person you just banned is very likely still signed in on their phone, and nothing you have done affects that.

This guide builds the flag and the login check, then tests the two things that decide whether the ban is real — what happens to a session that already exists, and whether the banned user can simply switch the flag back. Both were measured on 24 September 2026, with client keys, the way a real device would.

Architecture: the three routes a banned account can take into the backend, and the Cloud Code trigger that closes each one

Where does the ban live?

On the user record, as a boolean. Open the _User class in the Database browser, add a column named isBanned of type Boolean, and leave the default as false so new accounts start allowed.

That is the whole data model. No separate bans table, no join — the flag travels with the account, so every check that has the user has the answer.

The _User table with an isBanned column reading True for one account, undefined for another and False for the third

How do you block the login?

With a beforeLogin trigger, which runs before a session token is issued. That timing is the point: a banned account never receives one, so there is nothing to revoke.

Parse.Cloud.beforeLogin(async (request) => {
  if (request.object.get('isBanned')) {
    throw new Error('This user has been banned');
  }
});

Deploy that and the flag starts working immediately. A login for a flagged account comes back refused, with the message from the trigger:

{ "code": 141, "error": "This user has been banned" }

This has to live in Cloud Code rather than your app. A check in your own code runs on a device the user controls, and anyone can call the REST API directly with the same application and client keys. The trigger runs on the server whatever the client is.

Does blocking the login actually remove the user?

No — and the gap is the reason this guide continues. A ban that only runs at login does nothing to the people already inside.

Here is the test. Sign in as the user and keep the session token. Then set isBanned to true from the server and use that token again:

GET /users/me   with the token issued before the ban
→ 200   still valid, as alex_k

The banned account is still authenticated, still reading and writing, until it happens to log out. On a mobile app, where sessions are long-lived by design, that could be weeks.

How we measured: a session token was taken by logging in before the ban, the flag was then set from the server, and the same token was replayed against /users/me with the application id and REST key only — no master key, because the master key bypasses triggers and would prove nothing.

Blocking the door does nothing about the people already in the room.

It gets worse. Parse lets a signed-in user write to their own record — that is how “change my display name” works — and isBanned is a field on that record like any other. With the same pre-ban session:

PUT /users/<id>   { "isBanned": false }
→ 200   updated

GET /login        same credentials
→ 200   OK

The banned user unbanned themselves. Not through a flaw in Parse, but because the flag was left as ordinary user-writable data.

How do you make the ban stick?

Three triggers instead of one. The login check stays; two more close what it leaves open.

// 1. The door. beforeLogin runs before a session token is issued.
Parse.Cloud.beforeLogin(async (request) => {
  if (request.object.get('isBanned')) {
    throw new Error('This user has been banned');
  }
});

// 2. The flag. Without this, a signed-in user can clear their own ban.
Parse.Cloud.beforeSave(Parse.User, async (request) => {
  if (request.master) return;              // server-side and dashboard writes
  const original = request.original;
  if (!original) return;                   // sign-up, nothing to compare
  if (request.object.get('isBanned') !== original.get('isBanned')) {
    throw new Error('isBanned can only be changed by the server');
  }
});

// 3. The sessions already out there.
Parse.Cloud.afterSave(Parse.User, async (request) => {
  const original = request.original;
  if (!original) return;
  const nowBanned = request.object.get('isBanned') === true;
  const wasBanned = original.get('isBanned') === true;
  if (!nowBanned || wasBanned) return;

  const query = new Parse.Query(Parse.Session);
  query.equalTo('user', request.object);
  const sessions = await query.find({ useMasterKey: true });
  await Parse.Object.destroyAll(sessions, { useMasterKey: true });
});

Trigger 2 compares the incoming value with request.original and refuses only when the field actually changes, so ordinary profile updates still work. request.master lets the server and the dashboard through — otherwise you could not ban anyone.

Trigger 3 fires only on the transition into banned, not on every save of a banned user, which keeps it from re-querying sessions on unrelated writes.

The three triggers in the Cloud Code editor, ready to deploy

Do the three triggers close both holes?

Yes, on both counts, and the same tests say so. With all three deployed, a fresh session was taken before banning — the strongest case for an attacker.

TestOne triggerThree triggers
Login while banned400, code 141400, code 141
Pre-ban session, GET /users/me200 — still valid400, code 209 — invalid session token
Client changing isBanned200 — written400 — refused

Code 209 is Parse’s invalid-session-token error: the token the client is holding no longer exists, so the app sees a logged-out user and sends them to the sign-in screen, where trigger 1 turns them away.

How we measured: each row is one request per version of the Cloud Code — the video’s single trigger, then the three — run against the same account on the same backend on 24 September 2026, always with client keys.

How do you unban someone?

Set the flag back to false from anywhere the master key applies — the dashboard, a Cloud Code function, your admin tool. Trigger 2 lets it through because request.master is true; trigger 3 ignores it because it only acts on the transition into banned.

The user’s old sessions stay destroyed, which is correct: unbanning restores the right to sign in, not the sessions that were cancelled. They log in again and get a new one.

If you want bans that expire, store a date instead of a boolean and compare it in beforeLogin. The rest of the structure is unchanged, and the same two holes need the same two triggers.

Which errors will you see, and what do their bodies say?

Three, and all of them arrive as HTTP 400 with a Parse code in the body. The code is what your client should branch on; the message is yours, thrown by the trigger.

{ "code": 141, "error": "This user has been banned" }
{ "code": 141, "error": "isBanned can only be changed by the server" }
{ "code": 209, "error": "Invalid session token" }

Code 141 is the generic “a Cloud Code function failed” error, so both of your own rejections share it — the message is the only thing that tells them apart. If you need the client to react differently to each, throw a structured message and parse it, rather than matching on English text you may want to translate later.

Code 209 is the one your app already knows: it is what Parse returns for any dead session. Most clients treat it as “signed out” and send the user to the login screen, where the first trigger meets them with the ban message. That is the sequence a banned user actually experiences.

How we measured: each line above is the verbatim response body from a request made with the application id and REST key only, captured on 24 September 2026.

When is a ban flag the wrong tool?

When the person, not the account, is the problem. The flag lives on one user record, so someone who signs up again with a new e-mail walks straight back in — and nothing in this guide slows that down. If you are dealing with determined abuse rather than a rule-breaker you can name, you need signals that outlive an account, and that is a different and much harder design.

It is also the wrong shape for anything temporary. A boolean has no end date, so a seven-day suspension becomes a diary entry and somebody’s job to remember. Store a timestamp instead and compare it in beforeLogin; the two other triggers stay exactly as they are.

And it is too blunt when the problem is one feature. If someone is abusing comments, banning the account takes away everything else they do legitimately. A per-feature flag checked in the Cloud Code that owns that feature keeps the punishment the size of the offence.

What would you add to the ban system next?

Record who banned whom and why. A boolean tells you nothing in three months, and moderation decisions get appealed — a bannedAt, bannedBy and banReason on the same record cost nothing and answer the question later.

Consider what a banned session should see at the moment it dies. The client gets code 209 and, if your app treats that as “signed out”, the user simply lands on the login screen with no explanation. Catching that code and showing the ban message is a kinder ending than a silent logout.

And check the class-level permissions on _User while you are here. The triggers protect the one field; they do not stop a user reading other people’s records if the permissions allow it.

Tagsparse-servercloud-codeauthenticationsessionssecuritytriggers

Frequently asked questions

How do you ban a user in Parse Server?

Add a boolean field to the user record and check it in a beforeLogin trigger, which runs before a session token is issued. On September 24, 2026 a banned account's login returned HTTP 400 with code 141 and the message thrown by the trigger. That blocks new logins, but on its own it does not touch sessions the user already has, and it does not stop the user clearing the flag themselves.

Does banning a user log them out of Parse Server?

Not by itself. A session token issued before the ban keeps working: in a measured test, GET /users/me with the pre-ban token still returned HTTP 200 and the user's record after the ban was applied. To end it, query the _Session class for that user and destroy the results with the master key when the flag flips — after which the same request returns HTTP 400 with code 209, invalid session token.

Can a banned user unban themselves?

Yes, unless you stop it. Parse lets a signed-in user write to their own record, so with a session obtained before the ban, a PUT setting isBanned to false returned HTTP 200 and the next login succeeded. A beforeSave trigger on Parse.User that rejects any client-side change to the field closes it: the same request then returns HTTP 400 and the write is refused.

What happens to users created before you added the ban field?

They have no value at all, which reads as undefined rather than false. That is harmless for this check, because undefined is falsy and the trigger lets them through, but it means you cannot query for isBanned equal to false and expect every allowed user back. Query for the banned ones instead, or backfill the field.

Should the ban check live in Cloud Code or in the client?

Cloud Code, always. A check in your app's code runs on a device the user controls and can be skipped by anyone calling the REST API directly with the same keys. beforeLogin, beforeSave and afterSave run on the server on every request regardless of which client made it, which is what makes the rule a rule rather than a suggestion.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Sep 24, 2026 — Backend: Back4app, free plan, USA East · Client: REST API with the application and REST keys only · Terminal: curl and node on macOS.

More from Back4app Engineering

Backend · tutorial

Your Frontend Talks Straight to the Database. Here Is How to Lock It Down [Full Repo]

Backend · tutorial

How to Add Authentication to a Web or Flutter App Without Writing an Auth Server [Full Repo]

Backend · tutorial

Build and Deploy a Node.js REST API in 15 Minutes — Without Owning a Server [Full Code Inside]