Auth & page access

Auth & page access

Two pieces of plumbing every Cobuntu integration leans on:

  1. cobuntu_auth token — the cookie/JWT that says "this visitor is signed in to Cobuntu".
  2. page-access endpoint — the bulk API that says, per canonical Cobuntu page, "can THIS visitor see it?"

Either alone is incomplete. Together they let a customer landing mirror exactly what cobuntu-admin's visibility config says, without hardcoding any "is this page members-only?" opinions on the client.


Login methods

A visitor becomes signed in to Cobuntu through any of these, and the resulting cobuntu_auth token is identical regardless of which they used — your integration never needs to branch on how someone signed in:

MethodHow it works
Email + passwordClassic credentials.
Phone (one-time code)The user enters a phone number and verifies a 6-digit SMS code. The number is used only for sign-in + account security — it's never shown on a profile or to other members.
Linked account (Google / Slack / Discord)The user authorizes Cobuntu to read a basic profile (id, name, email, avatar) from the provider and links it to their Cobuntu account.

All of them land the same cobuntu_auth cookie described below.


The token

Cookie name

The auth token is set as a cookie by Cobuntu after a successful /login round-trip. The cookie name has three accepted forms:

Cookie nameWhen it's used
cobuntu_auth_<community-tag>Scoped per-community (post 2026-05-25). Preferred.
cobuntu_authCanonical fallback. Set by Cobuntu's central login flow.
cobuntu_auth_<bare-hostname>Older browsers might still hold this from before the scoped form rolled out.

Same-origin policy means any of these are readable from JavaScript on the customer's apex (e.g. yourdomain.com) when the cookie was set by the Cobuntu reverse proxy on that same apex. The token is a Bearer-style JWT — three base64-encoded segments separated by dots:

<header>.<payload>.<signature>

Payload

You can decode the middle segment in the browser (it's base64 JSON):

function readPayload(cookie: string) {
  const [, payload] = cookie.split(".");
  const padded = payload.replace(/-/g, "+").replace(/_/g, "/")
                  + "===".slice((payload.length + 3) % 4);
  return JSON.parse(atob(padded)) as {
    userId: string;
    iat: number;
    exp: number;
  };
}

Three fields you can trust client-side:

FieldTypeMeaning
userIdUUID stringCobuntu's identifier for this user across all communities they're in.
iatUnix secondsIssued-at timestamp.
expUnix secondsExpiry. Refresh-on-activity extends this; tokens are ~30 days.

What the token tells you alone

QuestionToken alone?
Is the visitor signed in to Cobuntu?✅ Yes (cookie present + exp > now)
What's their userId?✅ Yes (read payload.userId)
Are they a member of this community?❌ No — token is global, not community-scoped
What pages can they see?❌ No — depends on community config + membership
Should this signature be trusted?❌ No — only the server can verify; treat client-side as a presumption

What you should NOT do with the token

  • Don't trust it as proof of membership. A user with a valid Cobuntu token can be a member of community A but not community B. The token is identity, not authorization.
  • Don't verify the signature client-side. The signing key isn't (and can't be) public. Treat the token as a hint; the server verifies on every authenticated call.
  • Don't pass it to third parties. Same-origin cookies stay on Cobuntu's apex. If you need to call a non-Cobuntu API on behalf of the user, mint a separate session.

What the public API exposes about a user

Public, unauthenticated endpoints return only public profile fields — never private contact data:

Returned publiclyNever returned publicly
id, name, usertag, profile image, bio, pronouns, socials, public countsemail, phone number, account/verification status, payment identifiers

Private fields are reachable only by:

  • the owner, via authenticated /api/users/me* endpoints, and
  • a community's leaders, via authenticated, role-gated admin endpoints (e.g. member management) — and only for members of their own community.

So a public profile fetch — or a READ_PUBLIC API key listing community members — is safe to render anywhere: it carries no email or phone.


The page-access endpoint

The bulk visibility/membership oracle. One GET, full answer for every canonical page.

Endpoint

GET /api/communities/:tag/page-access
Cookie: cobuntu_auth_<tag>=<jwt>     (or anon — auth-optional)

Response

{
  "FEED":        { "allow": false, "reason": "page_members_only", "resolvedVisibility": "MEMBERS_ONLY" },
  "MEMBERS":     { "allow": false, "reason": "page_members_only", "resolvedVisibility": "MEMBERS_ONLY" },
  "ATLAS":       { "allow": false, "reason": "page_members_only", "resolvedVisibility": "MEMBERS_ONLY" },
  "BLOG":        { "allow": true,  "resolvedVisibility": "PUBLIC" },
  "EVENTS":      { "allow": true,  "resolvedVisibility": "PUBLIC" },
  "MARKETPLACE": { "allow": true,  "resolvedVisibility": "PUBLIC" },
  "CHAT":        { "allow": false, "reason": "page_members_only", "resolvedVisibility": "MEMBERS_ONLY" }
}

Sample above is the verdict for an anonymous visitor on a Hybrid Blocklist community with FEED / MEMBERS / ATLAS gated and BLOG / EVENTS / MARKETPLACE inherited as PUBLIC.

What the server actually does

Three composed checks per page key, one membership query for the whole batch. No per-key fan-out.

Discriminated reason field

allow: false always carries a reason so your gate copy can be specific:

reasonMeansSuggested UX
community_privateThe whole community is MEMBERS_ONLY. Joining unlocks everything."This community is members-only. [Apply to join]"
page_members_onlyThe community itself is public; this specific page (or CHAT intrinsic) requires membership."This page is members-only. [Sign in / join]"
page_disabledThe leader turned this page off entirely. The Cobuntu reverse proxy yields the route to the landing host (so the landing can author its own page at that path).Don't show the nav item at all. If the landing reaches this verdict via the SDK, treat as "page doesn't exist in Cobuntu" and let the landing's own router decide what to render.

The resolvedVisibility field is what the page would render as if the gate were lifted — useful when picking SEO <meta robots> flags upstream.


SDK helpers

Both wrappers ship from @cobuntu/widgets:

npm install @cobuntu/widgets

React: usePageAccess(communityTag)

For landings that already run React (Lovable, Next.js, Vite + React, etc.).

import { usePageAccess } from "@cobuntu/widgets";
 
function CommunityNav() {
  const { canSee, status } = usePageAccess("your-community");
 
  if (status === "loading") return <NavSkeleton />;
 
  const items = [
    { label: "Eventos",     href: "/events",      cobuntuPage: "EVENTS" },
    { label: "Marketplace", href: "/marketplace", cobuntuPage: "MARKETPLACE" },
    { label: "Feed",        href: "/feed",        cobuntuPage: "FEED" },
    { label: "Membros",     href: "/members",     cobuntuPage: "MEMBERS" },
    { label: "Mapa",        href: "/atlas",       cobuntuPage: "ATLAS" },
  ];
 
  return (
    <nav>
      {items
        .filter(i => !i.cobuntuPage || canSee(i.cobuntuPage))
        .map(i => <a key={i.href} href={i.href}>{i.label}</a>)}
    </nav>
  );
}

What the hook does for you:

  • Lazy fetch on mount. First call resolves; subsequent usePageAccess("same-tag") instances reuse the cached map.
  • Refetch on window focus. A visitor who logs in on another tab gets their member-only nav items revealed on focus return.
  • Fail-closed canSee(). While loading, on network errors, or for an unknown key, canSee returns false. Better to hide a tile briefly than to surface one that 401s on click.
  • Manual refresh(). Call this after a successful login flow inside the same SPA tab so the hook re-fetches without waiting for a focus event.

Vanilla: fetchPageAccess(communityTag)

For non-React landings (Squarespace, Webflow, plain HTML).

<script type="module">
  import { fetchPageAccess } from "https://cdn.cobuntu.com/widgets/v1/sdk.js";
 
  const access = await fetchPageAccess("your-community");
  document.querySelectorAll("[data-cobuntu-page]").forEach((el) => {
    const key = el.dataset.cobuntuPage;
    if (!access[key]?.allow) el.style.display = "none";
  });
</script>
 
<nav>
  <a href="/events"      data-cobuntu-page="EVENTS">Eventos</a>
  <a href="/marketplace" data-cobuntu-page="MARKETPLACE">Marketplace</a>
  <a href="/feed"        data-cobuntu-page="FEED">Feed</a>
  <a href="/members"     data-cobuntu-page="MEMBERS">Membros</a>
  <a href="/atlas"       data-cobuntu-page="ATLAS">Mapa</a>
</nav>

The vanilla helper does no caching — call it once on page load and reuse the result. Pair with a window.addEventListener ("focus", ...) for the same refetch-on-focus behaviour the React hook gives you for free.


Widget authed endpoints (v0.5.0+)

From @cobuntu/widgets@0.5.0 onwards, every authed call the drop-in widget bundles make (cart, auth, messages, notifications) goes to a same-origin relative path by default — not to https://api.cobuntu.com.

Why same-origin

The cobuntu_auth cookie is host-scoped to the customer's apex. A cross-origin fetch from a widget on yourdomain.com to api.cobuntu.com carries no cookie even with credentials: "include" — same-origin policy. Same-origin via the Cobuntu reverse proxy makes the cookie travel server-side, which works on every apex without per-customer setup.

The path mirrors what fetchPageAccess / usePageAccess (v0.4.0+) already did for the bulk visibility endpoint — extended to every authed endpoint the widget bundles use.

Endpoints covered

The Cobuntu reverse proxy resolves these paths on every customer apex into thin per-endpoint forwarders that attach the visitor's cobuntu_auth cookie as Authorization: Bearer … to the BE:

MethodPathUsed by
GET/api/users/meauth widget (avatar + name)
GET/api/users/me/memberships/cobuntunav widget (member-only items)
GET/api/users/me/notificationsnotifications drawer
GET/api/users/me/notifications/unread-countnotifications badge poll
PATCH/api/users/me/notifications/mark-readmark-all-read button
GET/api/users/me/friend-requests/incomingnotifications drawer
POST/api/users/me/friends/:usertag/acceptaccept friend request
DELETE/api/users/me/friends/:usertag/rejectreject friend request
POST/api/users/me/checkoutcart checkout
GET/api/messaging/conversations/unread/countsmessages badge poll
GET/api/communities/:tag/page-accesspage-access SDK (v0.4.0+)
GET/api/communities/by-domain/:hostpublic bootstrap (stays absolute, no auth)

Cookie-flow prerequisite

For widgets to detect a signed-in viewer, the user must sign in on the apex itself — e.g. yourdomain.com/login, which proxies through to Cobuntu's branded login page and lands the cobuntu_auth cookie on yourdomain.com. Signing in on app.cobuntu.com or any <community>.cobuntu.com subdomain lands the cookie on that host instead, which the widget on your apex can't read.

If your landing's "Sign in" affordance points anywhere other than your own /login, the widgets render as logged-out for the user even when they have an active Cobuntu session — that's not a widget bug, it's the cookie not being on the apex.

Standalone non-proxied consumers

A site that loads the IIFE bundles without Cobuntu's reverse proxy in front (e.g. an internal dashboard on a non-Cobuntu apex, no community-app middleware) can opt out via the WIDGET_API_URL env var at build time:

WIDGET_API_URL=https://api.cobuntu.com npm run build:widgets

That bakes __WIDGET_API_URL__ into the IIFE so every authed call goes back to absolute URLs. Those consumers also need to handle their own auth-cookie forwarding to api.cobuntu.com (CORS + cookie domain) — cross-origin cookies aren't sent automatically.

In practice the absolute mode is rarely needed: 99% of Cobuntu customers have their apex reverse-proxied through community-app (Hosted apex pattern), and the default same-origin path is exactly what works there.


Putting it together

Three viewer states, one decision tree:

That's the full integration story for nav-level visibility. For backend-to-backend automation (no visitor cookie), see REST API authentication — the X-API-Key flow used by headless entry apps.