BuildBot

Auth & Access

Gating content the right way

Lesson 7 of 7

What you'll learn

  • Tell the difference between UI gating and real enforcement
  • Enforce access where the data lives
  • Avoid the most common security mistake in gated apps

It's tempting to hide a paid lesson with a component and call it done:

<Protect plan="pro">
  <PremiumLesson />
</Protect>

This is fine for UX — it stops the button from showing. But it is not security. Components like <Protect> only decide what to render; a determined user can still call your backend directly. The data must be guarded where it's served.

Enforce in the function

The real check belongs inside the Convex query or mutation, next to the data:

export const getPremiumLesson = query({
  args: { lessonId: v.string() },
  handler: async (ctx, { lessonId }) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Sign in required");

    const entitled = await hasEntitlement(ctx, identity.subject);
    if (!entitled) throw new Error("Upgrade required");

    return ctx.db.get(lessonId);
  },
});

The golden rule

Gate the experience in the UI for speed and polish. Gate the data on the server for safety. If you only do one, do the server.

Two layers, two jobs

| Layer | Tool | Purpose | |---|---|---| | UI | <Protect>, has() | Fast, optimistic hide/show | | Backend | check inside Convex fn | The actual access boundary |

A robust pattern syncs entitlements (from a billing webhook, say) into a Convex table, then every sensitive function reads that table. The UI can be wrong or stale; the server is the source of truth.

The exercise shows why client-only gating fails: the "UI" hides the link, but a direct backend call still has to be rejected on its own.

Why server checks are non-negotiable

The UI hides premium content from anonymous users — but a direct fetch must ALSO be blocked. Run it and see both paths.

Loading editor…

That's the full loop: render on the server, store and sync with Convex, prove identity with Clerk, and enforce access where it counts. Mark this complete — you've built the mental model for a real app.

Sign in to save your progress across devices.