Blog /
dsgo.user.can() deep dive: gating UI without writing auth
Every app that does anything interesting eventually needs the same conditional: show this button if the user is allowed to use it. Hide that section if they are not. Render the admin tab only for admins. Show the “edit” link to authors but not to subscribers.
In a React app on Vercel, this is a custom permissions library, a role table you keep in sync with your auth provider, and a useCan() hook you import everywhere. In a DesignSetGo App, it is one line:
const canEdit = await dsgo.user.can('edit_posts');
The bridge asks WordPress whether the current visitor has that capability. WordPress already knows. It has known since before your app loaded.
This post is the working tour: the method signature, three real examples, and the gotcha that catches everyone the first time.
What dsgo.user.can() actually does
The signature:
dsgo.user.can(capability: string): Promise<boolean>
The bridge sends a postMessage to the WordPress parent. The parent calls WordPress’s current_user_can() function with the capability string you passed. The result comes back as a boolean.
Three things to know about it.
It uses the visitor’s identity, not your app’s. There is no app identity. The bridge runs as whatever WordPress user is currently logged into the site. A logged-out visitor gets false for every capability check that requires a role. A subscriber gets true for read and not much else. An editor gets true for edit_posts, edit_others_posts, publish_posts. An admin gets everything.
It is server-evaluated. You cannot lie to it by editing JavaScript. The check happens on the WordPress side, against the real WordPress session cookie. If the visitor changes the value in their browser dev tools, the next call returns the truth.
It honors custom capabilities from other plugins. This is the part that surprises people. WordPress capabilities are not a fixed list. WooCommerce registers manage_woocommerce. Yoast registers SEO-related caps. Membership plugins register subscription-tier caps. ACF registers field-group caps. Your app can ask about any of them:
const canManageStore = await dsgo.user.can('manage_woocommerce');
const canSeeAnalytics = await dsgo.user.can('view_woocommerce_reports');
If the site has WooCommerce installed, those return the right answer. If not, they return false. Your app does not need to know which plugins are installed; it just asks.
Example 1: a capability-gated button
The simplest pattern. Show an “edit this” button only to users who can edit:
import { dsgo } from '@designsetgo/app-client';
import { useEffect, useState } from 'react';
function EditButton({ postId }: { postId: number }) {
const [canEdit, setCanEdit] = useState(false);
useEffect(() => {
dsgo.user.can('edit_posts').then(setCanEdit);
}, []);
if (!canEdit) return null;
return <a href={`/wp-admin/post.php?post=${postId}&action=edit`}>Edit</a>;
}
A subscriber visiting the page never sees the button. An editor does. No app-side logic, no role-to-capability mapping, no list of admin user IDs hardcoded in your bundle.
Example 2: an admin-only tab in a multi-tab app
A dashboard with a public “Stats” tab and an admin-only “Settings” tab:
function Dashboard() {
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
dsgo.user.can('manage_options').then(setIsAdmin);
}, []);
return (
<Tabs>
<Tab title="Stats"><StatsView /></Tab>
{isAdmin && <Tab title="Settings"><SettingsView /></Tab>}
</Tabs>
);
}
manage_options is WordPress’s “you can change site settings” capability, which in practice maps to administrators. If you want “editor or above” instead, use edit_others_posts. If you want “anyone who can publish,” use publish_posts.
The WordPress roles and capabilities reference is the canonical list, and Tuesday’s roles primer covers which five you will actually use.
Example 3: gating a bridge call before you make it
The bridge will reject calls your manifest does not allow. Capability checks let you also gate calls your manifest does allow but only some users should make:
async function publishStatusUpdate(message: string) {
if (!(await dsgo.user.can('edit_posts'))) {
throw new Error('You do not have permission to publish updates.');
}
await dsgo.posts.create({ title: 'Status', content: message, status: 'publish' });
}
You could also let the call fail and show the error from the server. Checking first lets you produce a friendlier message and avoid the network round-trip. Both are fine. The capability check is a UX courtesy; the actual enforcement is in WordPress.
The gotcha that catches everyone
The capability you pass is the capability, not the role. WordPress’s capability strings are things like edit_posts, publish_posts, manage_options, delete_users. They are not editor, admin, contributor.
If you write:
const isAdmin = await dsgo.user.can('administrator');
That always returns false, because administrator is a role, not a capability. The role contains a bundle of capabilities. The check is on the capability, not the role.
The mental shift: think of capabilities as verbs (edit_posts, manage_options, view_woocommerce_reports). Roles are nouns (administrator, editor, subscriber). Your app asks “can this user do this verb?”, not “is this user this noun?”. The verb survives custom roles, multi-site setups, role-management plugins, and the inevitable “wait, why is this client a Shop Manager?” conversation. The noun does not.
If you genuinely need the role, get it from dsgo.user.current().roles. But check the capability when you can.
When to use it, and when not to
Use dsgo.user.can() for every UI element whose visibility or behavior depends on the visitor’s permissions. It is cheap (cached on the WordPress side), it is honest (it asks the actual auth system), and it is portable (it works the same on every site that installs your app).
Do not use it as a security boundary by itself. Capability checks in your app are UX. The bridge enforces permissions on the WordPress side independently: if your manifest does not declare permissions.write: ['posts'], your app cannot publish a post no matter what the visitor’s capabilities are. The two layers compose. The manifest says “what this app is allowed to do.” The capability check says “what this visitor is allowed to do.” Both have to be true for the call to succeed.
Where this goes next
dsgo.user.can() is the gateway to most of the patterns in The bridge cookbook. Once you internalize “ask WordPress, do not invent your own roles,” the rest of the bridge API gets a lot smaller.
If you are starting a new app, the member portal example uses dsgo.user.can() in three different places and is a good copy-paste reference. The full method list lives in BRIDGE-API.md.