@axcode/axcloud-sdk
SDK for building websites with AxCloud CMS and member management. AxCloud is headless: it owns content, publication, access control and delivery APIs, while the consuming application owns markup, layout and design.
Installation
npm install @axcode/axcloud-sdk
Version Tags
The SDK is published with two tags for different environments:
| Tag | Purpose | Stability | Install |
|---|---|---|---|
latest | Production | Stable, tested | npm install @axcode/axcloud-sdk |
next | Development/Staging | Preview, may have breaking changes | npm install @axcode/axcloud-sdk@next |
Recommended production setup:
Resolve latest deliberately, then commit the exact version and lockfile:
npm install --save-exact @axcode/axcloud-sdk@latest
{
"dependencies": {
"@axcode/axcloud-sdk": "0.3.1"
}
}
Do not install a moving tag in CI or during deployment. Test a specific next
version in a branch when needed, then promote and pin the stable release.
Quick Start
Anonymous public sites (no Auth0)
Public product sites that do not have member login should import the dedicated provider. It mounts the SDK client, React Query cache and privacy-safe analytics without importing or initializing Auth0:
import { AxCloudPublicProvider } from '@axcode/axcloud-sdk/public';
<AxCloudPublicProvider
orgId="barnsport"
apiUrl="https://api.axcloud.app"
stage="prod"
apiKey="pk_live_barnsport_read_submit"
>
<App />
</AxCloudPublicProvider>;
Public websites can read published CMS content through the same Auth0-free entrypoint. Draft and preview content remain available only through the authenticated Console:
import { usePageContent } from '@axcode/axcloud-sdk/public';
function HomeHero() {
const content = usePageContent('home', { lang: 'sv', publicOnly: true });
return <h1>{content.text('hero.title', 'Fallback title')}</h1>;
}
For non-React adapters and incremental migrations, create the same scoped Barnsport client without importing the authenticated SDK root:
import { createPublicBarnsportClient } from '@axcode/axcloud-sdk/barnsport';
const barnsport = createPublicBarnsportClient({ apiUrl, apiKey, orgId: 'barnsport' });
const providers = await barnsport.listProviders({ q: 'fotboll', limit: 5 });
Use a browser-safe key restricted to the public scopes needed by the site. Do
not use AxCloudPublicProvider for member or administrator features. Existing
authenticated applications continue to use AxCloudProvider unchanged.
Barnsport product client and hooks
Barnsport types, the core client and React hooks are available from a separate entrypoint so other products do not load Barnsport code:
import {
useBarnsportProviderSearch,
useBarnsportProvider,
useSubmitBarnsportReview,
useTrackBarnsportEvent,
} from '@axcode/axcloud-sdk/barnsport';
const providers = useBarnsportProviderSearch({ q: searchText, limit: 5 });
const provider = useBarnsportProvider(providerSlug);
Provider reviews and product events remain Barnsport domain data. The SDK routes them through the AxCloud Barnsport proxy; it does not store them in the generic Content service.
1. Wrap your app with AxCloudProvider
import { AxCloudProvider } from '@axcode/axcloud-sdk';
function App() {
return (
<AxCloudProvider
orgId="org_ba81807b" // Your organization ID
apiUrl="https://api.axcloud.app"
apiKey="pk_live_..." // Public API key from Settings → API Keys
auth0={{
domain: 'auth.axcloud.app',
clientId: 'your-client-id',
redirectUri: window.location.origin,
organization: 'org_auth0_...', // Optional Auth0 Organization ID for invite-only member sites
}}
>
<Router>
<Routes />
</Router>
</AxCloudProvider>
);
}
orgId is the AxCloud organization ID used by CMS APIs, for example org_ba81807b.
auth0.organization is the Auth0 Organization ID used during login, for example org_o960yILn77d4vY6u.
Set auth0.organization for invite-only customer member sites so Universal Login is scoped to that customer organization.
Customer-owned identity providers
An organization may authenticate through its own Auth0 tenant while AxCloud continues to provide content, membership and authorization APIs. Configure the complete, reviewed provider tuple statically in the first-party site:
<AxCloudProvider
orgId="org_customer"
apiUrl="https://dev-api.axcloud.app"
stage="dev"
auth0={{
identityProviderId: 'idp_customer_dev',
domain: 'dev-auth.customer.example',
clientId: 'customer-member-spa-client-id',
audience: 'https://api.axcloud.app',
organization: 'org_customer_auth0',
redirectUri: window.location.origin,
}}
>
<App />
</AxCloudProvider>
identityProviderId is a public AxCloud identifier, not a credential. It is
included in the SDK cache-owner signature so a provider switch or rotation
cannot reuse identity-bound query data. The SDK does not discover providers or
hold Auth0 Management API credentials. Keep domain, client ID, audience,
organization ID, AxCloud stage and API URL together as one environment-specific
configuration; never mix a customer DEV tenant with AxCloud PROD APIs.
2. Fetch content
import { useNews } from '@axcode/axcloud-sdk';
function NewsPage() {
const { data: news, isLoading, isError } = useNews();
if (isLoading) return <Spinner />;
if (isError) return <Error />;
return (
<ul>
{news.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
Content delivery: These hooks return only published snapshots. When the user is authenticated, access to internal, member and section content is derived from the verified token. Anonymous users receive only public content. Client-supplied status or section identifiers never grant broader access.
Pages and news can be stored once in AxCloud with translations and requested per language:
import { useContentItem, useNews } from '@axcode/axcloud-sdk';
function AboutPage({ locale }: { locale: 'sv' | 'en' }) {
const { data: page } = useContentItem('pages', 'about-us', {
lang: locale,
publicOnly: true,
});
return <h1>{page?.title}</h1>;
}
function NewsList({ locale }: { locale: 'sv' | 'en' }) {
const { data: news } = useNews({ lang: locale });
return news.map((item) => <article key={item.id}>{item.title}</article>);
}
3. Add authentication
import { useAuth, LoginButton, LogoutButton, ProtectedRoute } from '@axcode/axcloud-sdk';
function Header() {
const { isAuthenticated, user } = useAuth();
return (
<header>
{isAuthenticated ? (
<>
<span>Welcome, {user.name}</span>
<LogoutButton />
</>
) : (
<LoginButton>Sign In</LoginButton>
)}
</header>
);
}
function MemberArea() {
return (
<ProtectedRoute>
<MemberContent />
</ProtectedRoute>
);
}
The SDK keeps verification UI inside the product site. Render your own branded
notice when needsEmailVerification is true:
const { needsEmailVerification, requestEmailVerification, refreshUser } = useAuth();
if (needsEmailVerification) {
// Render the product's own verification notice.
}
await requestEmailVerification({ returnTo: window.location.href });
await refreshUser();
The API resolves the recipient and organization from the authenticated token. Return URLs are restricted to the organization's configured member site.
3b. Add privacy-safe analytics
Analytics is optional and provider-independent. AxCloud resolves the public tracker configuration from the organization, site key, deployment stage, and current hostname. SDK consumers never receive the Umami API key.
import { AxCloudProvider, useAnalytics } from '@axcode/axcloud-sdk';
<AxCloudProvider
orgId="org_ba81807b"
apiUrl="https://api.axcloud.app"
stage="prod"
analytics={{
enabled: analyticsConsent === 'accepted',
siteKey: 'member-site',
normalizePath: (path) => path.replace(/^\/member\/news\/[^/]+/, '/member/news/:id'),
}}
auth0={auth0Config}
>
<App />
</AxCloudProvider>;
The SDK tracks SPA page changes after the tracker is ready. Query strings and
fragments are removed, common opaque IDs are replaced with :id, Do Not Track
is honored, and localhost collection is disabled unless trackLocalhost: true
is explicitly set.
Custom events must be allowlisted in the AxCloud analytics site configuration:
function NewsArticle() {
const analytics = useAnalytics();
const handleOpen = () => {
analytics.track('news_opened', { surface: 'member_news' });
};
}
Only send coarse enums, booleans, and counts. The SDK rejects sensitive property names and email-like values, but applications remain responsible for never supplying names, email addresses, user IDs, document titles, search text, chat text, or other personal/content data. Production trackers should only be enabled after the product's privacy notice and Content Security Policy have been reviewed.
4. Use sections
import { useMySection } from '@axcode/axcloud-sdk';
function MySectionPage() {
const { section, members, isLoading } = useMySection();
if (isLoading) return <Spinner />;
if (!section) return <p>You are not assigned to a section</p>;
return (
<div>
<h1>{section.name}</h1>
<h2>Members ({members.length})</h2>
<ul>
{members.map((m) => (
<li key={m.id}>{m.name}</li>
))}
</ul>
</div>
);
}
4b. Section-scoped content
import { useNews, useMySection } from '@axcode/axcloud-sdk';
function SectionNews() {
const { section } = useMySection();
// AxCloud derives the allowed sections from the authenticated membership.
const { data: visibleNews } = useNews();
return (
<div>
<h2>Section News</h2>
{visibleNews.map((item) => (
<NewsCard key={item.id} item={item} />
))}
{!section && <p>You are not assigned to a section.</p>}
</div>
);
}
5. Fetch organization branding and media
import { useBranding, useLogo, useMedia } from '@axcode/axcloud-sdk';
function Header() {
const { logoUrl, branding, organizationName } = useBranding();
return (
<header style={{ backgroundColor: branding?.primaryColor }}>
{logoUrl && <img src={logoUrl} alt={organizationName} />}
<h1>{organizationName}</h1>
</header>
);
}
// Or just the logo
function Logo() {
const { logoUrl, isLoading } = useLogo();
if (isLoading) return <Skeleton />;
return logoUrl ? <img src={logoUrl} alt="Logo" /> : <DefaultLogo />;
}
// Fetch specific media file
function HeroImage() {
const { mediaUrl } = useMedia('hero-banner.jpg');
return mediaUrl ? <img src={mediaUrl} alt="Hero" /> : null;
}
Note: Media URLs are signed S3 URLs that expire after 1 hour. The SDK automatically refreshes them before expiry (staleTime: 30 min).
6. Fetch public documents
import { useDocuments, useDocumentDownload } from '@axcode/axcloud-sdk';
function PublicDocuments() {
const { documents, isLoading } = useDocuments({ tags: ['handbook'], limit: 20 });
if (isLoading) return <Skeleton />;
return (
<ul>
{documents.map((doc) => (
<li key={doc.document_id}>
<DocumentLink documentId={doc.document_id} title={doc.title} />
</li>
))}
</ul>
);
}
function DocumentLink({ documentId, title }: { documentId: string; title: string }) {
const { documentUrl } = useDocumentDownload(documentId);
return documentUrl ? <a href={documentUrl}>{title}</a> : <span>{title}</span>;
}
Only documents with status: "published" and audience: "public" are exposed through SDK document hooks.
7. Gate features by module
import { ModuleGate, useOrg } from '@axcode/axcloud-sdk';
function Dashboard() {
const { hasModule } = useOrg();
return (
<div>
<NewsSection />
<ModuleGate module="training" fallback={<UpgradePrompt />}>
<TrainingSection />
</ModuleGate>
{hasModule('ai') && <AIChatWidget />}
</div>
);
}
8. Membership applications with a section choice
import { createFormHelper } from '@axcode/axcloud-sdk';
const forms = createFormHelper({
apiUrl: 'https://api.axcloud.app/forms',
apiKey: import.meta.env.VITE_AXCLOUD_FORMS_API_KEY, // forms:submit only
site: 'example.org',
});
// Sections the organization ticked as selectable, e.g. [{ id: 'section_x', name: 'SAS' }]
const sections = await forms.getMembershipSections();
// Either the chosen section…
await forms.submitMembership(email, { firstName, lastName, sectionId: sections[0].id });
// …or a company without a section of its own.
await forms.submitMembership(email, { firstName, lastName, company: 'Example Air' });
The API refuses a sectionId that is not in the list. The approval in the console invites
the applicant into the chosen section. getMembershipSections throws when the API refuses
(for example when the membership form is not enabled), so a site can fall back to a free-text
company field. The call is checked with the forms key like a submission but does not count toward
the key's rate limit, and browsers may keep the answer for five minutes.
API Reference
Provider
| Component | Description |
|---|---|
AxCloudProvider | Main provider that combines Auth0, React Query, and SDK context |
Hooks
| Hook | Description |
|---|---|
useAuth() | Login/logout, email-verification state, resend, and profile refresh |
useUser() | Fetch the normalized user and verified organization membership |
useUpdateMyProfile() | Update the signed-in user's name, phone, and address |
useOrg() | Fetch organization info and check modules |
useNews(options?) | Fetch the published news collection visible to the current visitor |
useContent(type, options?) | Fetch a published news, pages, or events collection |
useContentItem(type, id, opts?) | Fetch one published item directly by ID or slug |
usePageContent(slug, options?) | Fetch a page and read structured template slots |
useMySection() | Fetch current user's section with members |
useSections() | Fetch all organization sections |
useBranding() | Fetch organization branding (logo, colors) |
useLogo() | Fetch organization logo URL |
useMedia(mediaId) | Fetch specific media file URL |
useAvatar(userId) | Fetch user avatar URL |
useMediaList(type) | List media files |
useDocuments(options?) | List published public documents |
useDocumentDownload(documentId) | Fetch a signed document download URL |
Profile self-service is deliberately limited to personal contact fields. Organization title, email, roles, status, organization, and section assignments remain administrator-managed.
useUser() normalizes the API response to camelCase. Organization-specific values always come
from the membership for the provider's orgId:
const {
user,
roleInOrg,
title,
section, // { id, name, role } | null
sections, // all verified section memberships
sectionId,
sectionIds,
memberSince, // normalized from joined_at
} = useUser();
return (
<p>
{section?.name ?? 'No section'} · member since {memberSince}
</p>
);
import { useUpdateMyProfile } from '@axcode/axcloud-sdk';
function ProfileForm() {
const updateProfile = useUpdateMyProfile();
const save = () =>
updateProfile.mutateAsync({
firstName: 'Anna',
lastName: 'Pilot',
phone: '+46701234567',
address: 'Flyggatan 1',
});
return (
<button onClick={save} disabled={updateProfile.isPending}>
Save profile
</button>
);
}
Content Query Options
interface ContentQueryOptions {
page?: number; // Page number (default: 1)
pageSize?: number; // Items per page (default: 20)
search?: string; // Search in title/content
sortBy?: 'publishedAt' | 'createdAt' | 'updatedAt' | 'title';
sortOrder?: 'asc' | 'desc';
tags?: string[];
lang?: string; // Return translated title/slug/blocks when available
publicOnly?: boolean; // Force public endpoint even when authenticated
}
Collection hooks return data, count, total, page, pageSize,
hasMore, nextCursor, isLoading, isFetching, isError, error and
refetch. During Auth0's initial session check, member-capable hooks wait
instead of briefly requesting public content. Member caches are isolated per
Auth0 subject and removed on logout or account change.
All other identity-bound AxCloud query data is removed at the same boundary.
Structured slots are untrusted CMS input. Prefer text(), list() and
image(). unsafeHtml() (and the deprecated html() alias) must be sanitized
before use with dangerouslySetInnerHTML.
Components
| Component | Description |
|---|---|
<LoginButton> | Trigger Auth0 login |
<LogoutButton> | Trigger logout |
<ProtectedRoute> | Require authentication to view content |
<ModuleGate> | Conditionally render based on enabled modules |
Advanced Usage
Custom API calls
import { useAuth } from '@axcode/axcloud-sdk';
function MyComponent() {
const { getAccessToken } = useAuth();
const fetchCustomData = async () => {
const token = await getAccessToken();
const response = await fetch('/api/custom', {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
};
}
Access the underlying Auth0 client
const { getAuth0Client } = useAuth();
const auth0 = getAuth0Client();
// Full Auth0 API available
await auth0.getAccessTokenSilently();
await auth0.getIdTokenClaims();
Security Model
API Key vs Authentication Token
The SDK uses two types of credentials:
-
API Key (
apiKeyprop): A public identifier for your organization- Safe to include in client-side code
- Used for public content endpoints
- Backend validates and rate-limits based on API key
- Without a valid API key, requests return 401
-
Authentication Token (via Auth0): A secret JWT token
- Automatically managed by the SDK
- Required for member-only content and user data
- Never exposed in client code
What the API Key Protects
| Without API Key | With API Key | With Auth Token |
|---|---|---|
| Nothing | Public news/content | Member content |
| Organization branding | User profile | |
| Public media | Linked accounts | |
| Admin functions |
Endpoint Security
| Endpoint Type | Authentication | Example |
|---|---|---|
| Public content | API key (x-api-key header) | /content/public/{orgId}/news |
| Member content | JWT Bearer token | /content/members/{orgId}/news |
| User data | JWT Bearer token | /api/users/me |
| Admin | JWT Bearer token + role | /forms/admin/submissions |
Error Handling
import { useNews, isAxCloudError } from '@axcode/axcloud-sdk';
function NewsPage() {
const { data, error, isError } = useNews();
if (isError) {
if (isAxCloudError(error)) {
switch (error.code) {
case 'UNAUTHORIZED':
return <LoginPrompt />;
case 'NOT_FOUND':
return <NotFound />;
default:
return <Error message={error.message} />;
}
}
return <Error message="Something went wrong" />;
}
return <NewsList items={data} />;
}
License
UNLICENSED - Internal use only. Contact Axcode AB for access.