Overview
A suspension puts a user in a platform-wide timeout. While an active suspension is in place, the user cannot participate — every content and participation write is blocked server-side — but they can still sign in, read everything, and manage their own account. It’s the “you’re in timeout” model (Reddit / Discord / StackOverflow), not an eviction. Suspensions are set by project owners and admins from the Sublay Dashboard, or automatically by the report-handling flow. They are platform-wide and distinct from space membership moderation (space bans), which is scoped to a single space and set by space admins.Requires the
moderation bundle. Suspensions are only available when the moderation bundle is installed on your project. On projects without it, suspensions are never queried and writes are never blocked on this basis. See Bundles to add it.What a suspension blocks
While a user has an active suspension, every authenticated participation and content write is blocked — it’s a denylist-by-default. This includes:- Creating, updating, or deleting comments — including editing or deleting your own
- Adding or removing reactions — on comments and entities
- Creating, updating, or deleting entities, and publishing drafts
- Creating or joining spaces
- Creating events and RSVP’ing
- Chat: creating conversations, sending messages, reacting to messages, reporting messages
- Collections writes (create, add entity, update, delete)
- Following, requesting a connection, and accepting or declining a connection
- Standalone file and image uploads
- Filing reports
What stays open
Reads, sign-in, and account self-management are never blocked by a suspension:- All reads (GET requests) — public and member-only content
- Sign in, refresh token, sign out, change password, account-deletion request/confirm
- Editing your own profile and deleting your own account
- Marking notifications read / mark-all-read
- Registering/deregistering push tokens and updating notification preferences
- Linking/unlinking OAuth identities
- Chat: marking as read and muting conversations
The blocked-write response contract
A blocked write returns HTTP403 with this body:
The
reason and endDate are taken from the user’s effective suspension (see below).
Effective suspension
Multiple active suspensions can coexist on one user (for example, a manual suspension and a report-driven auto-suspension). The effective suspension is the furthest-reaching active row — an indefinite suspension (endDate: null) reaches the furthest and always wins the tie-break. A user is considered suspended while any active suspension exists.
“Active” means startDate ≤ now and (endDate ≥ now or endDate is null).
SDK: consuming suspension state
The@sublay/core SDK only consumes the current user’s own suspension state — it does not suspend or lift (that’s the dashboard’s job). There are two surfaces.
Ambient state — useUser
useUser exposes derived isSuspended and activeSuspension fields, computed from the authenticated user’s suspensions using the same effective definition as the server. Bind your “you’re suspended until X” banner or disabled composer to these.
activeSuspension is { reason, startDate, endDate } | null — the furthest-reaching active suspension, or null when the user isn’t suspended.
Reactive error — SuspendedError
If a write is attempted anyway (for example, a suspension lands mid-session), the blocked 403 user/suspended response is surfaced as a typed, catchable SuspendedError carrying reason and endDate. The SDK discriminates this response before its generic 403 → token-refresh handling, so a blocked write is not silently retried and does not trigger a spurious token refresh.
isSuspendedError(err) (or err instanceof SuspendedError) to narrow. Both SuspendedError and isSuspendedError are exported from @sublay/core.
Dashboard flow
Suspending and lifting are done from the Sublay Dashboard by project owners and admins — no code integration is required.- View. A user’s suspensions are shown on their profile in the Users view — active ones prominently (reason, until-when, who set it), past ones as history. Indefinite suspensions (
endDate: null) show as active. - Suspend. Suspend a user with a reason and an optional expiration. Leaving the expiration empty creates an indefinite suspension.
- Lift. Lifting a suspension expires it in place (it isn’t deleted — the audit trail is preserved). Because multiple active suspensions can coexist, lifting one active row may leave others in place. A “lift all active” action fully un-suspends the user by clearing every active row at once.
Suspension has retroactive bite: enabling it can block users who were suspended before enforcement existed. Lifting is the escape hatch and ships alongside it.
What suspension deliberately does not do
- Reads are never blocked — a suspended user can read everything they could before.
- Sign-in is never blocked — so the user can see the reason/end date and appeal.
- There is no per-app or per-severity configuration. Suspension is a single, fixed behavior: block participation writes, allow reads and account self-management. There is no advisory level, no read-blocking mode, and no
mutevsbanseverity.
Related
- useUser hook —
isSuspended/activeSuspension - User data model — the
Suspensionshape onAuthUser - Moderation overview — reporting and content moderation
- Space membership — space-scoped bans (a separate axis)

