Auth & page access
Two pieces of plumbing every Cobuntu integration leans on:
cobuntu_authtoken — the cookie/JWT that says "this visitor is signed in to Cobuntu".page-accessendpoint — 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:
| Method | How it works |
|---|---|
| Email + password | Classic 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 name | When it's used |
|---|---|
cobuntu_auth_<community-tag> | Scoped per-community (post 2026-05-25). Preferred. |
cobuntu_auth | Canonical 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:
| Field | Type | Meaning |
|---|---|---|
userId | UUID string | Cobuntu's identifier for this user across all communities they're in. |
iat | Unix seconds | Issued-at timestamp. |
exp | Unix seconds | Expiry. Refresh-on-activity extends this; tokens are ~30 days. |
What the token tells you alone
| Question | Token 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 publicly | Never returned publicly |
|---|---|
id, name, usertag, profile image, bio, pronouns, socials, public counts | email, 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:
reason | Means | Suggested UX |
|---|---|---|
community_private | The whole community is MEMBERS_ONLY. Joining unlocks everything. | "This community is members-only. [Apply to join]" |
page_members_only | The community itself is public; this specific page (or CHAT intrinsic) requires membership. | "This page is members-only. [Sign in / join]" |
page_disabled | The 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/widgetsReact: 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,canSeereturnsfalse. 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:
| Method | Path | Used by |
|---|---|---|
GET | /api/users/me | auth widget (avatar + name) |
GET | /api/users/me/memberships/cobuntu | nav widget (member-only items) |
GET | /api/users/me/notifications | notifications drawer |
GET | /api/users/me/notifications/unread-count | notifications badge poll |
PATCH | /api/users/me/notifications/mark-read | mark-all-read button |
GET | /api/users/me/friend-requests/incoming | notifications drawer |
POST | /api/users/me/friends/:usertag/accept | accept friend request |
DELETE | /api/users/me/friends/:usertag/reject | reject friend request |
POST | /api/users/me/checkout | cart checkout |
GET | /api/messaging/conversations/unread/counts | messages badge poll |
GET | /api/communities/:tag/page-access | page-access SDK (v0.4.0+) |
GET | /api/communities/by-domain/:host | public 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:widgetsThat 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.