The Phoenix auth/identity/tenancy backend repo is being renamed arcadia-app → arcadia-core (its primary OTP app is already arcadia_core). Updates prose, doc paths, and git.sky-ai.com repo URLs. Deliberately leaves the Rust crate arcadia-app-client and host arcadia-app.internal (handled separately), and the kept namespace (issuer/release "arcadia"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.3 KiB
@crema/arcadia-core-client
Typed HTTP client + React bindings for the arcadia Phoenix API. Wraps the OpenAPI-spec'd surface at /api/v1 with auth (Bearer JWT and/or service-account API key), tenant context (X-Tenant-ID), idempotency keys, error normalization, and rate-limit-aware retry.
The client is stateless about how the JWT is obtained — callers pass getToken so token refresh and storage stay app-owned.
Public API
import {
createArcadiaClient,
ArcadiaProvider,
useArcadia,
useArcadiaClient,
ArcadiaError,
} from "@crema/arcadia-core-client";
Usage in a Crema app
In app/root.tsx, wrap providers:
import { ArcadiaProvider } from "@crema/arcadia-core-client";
<ArcadiaProvider
baseUrl={import.meta.env.VITE_ARCADIA_URL}
initialTenantId={tenantId}
getToken={() => sessionStorage.getItem("arcadia_token")}
onUnauthorized={() => navigate("/login")}
>
{children}
</ArcadiaProvider>;
In any component:
import { useArcadiaClient, ArcadiaError } from "@crema/arcadia-core-client";
function ResourceList() {
const arcadia = useArcadiaClient();
const [items, setItems] = useState([]);
useEffect(() => {
arcadia
.GET<{ data: Resource[] }>("/api/v1/digital_objects", { params: { page: 1 } })
.then((res) => setItems(res.data))
.catch((err: ArcadiaError) => {
if (err.isRateLimited) showToast("Too many requests, slow down.");
});
}, [arcadia]);
}
Generated types
Endpoint request/response types come from arcadia's live OpenAPI spec. Regenerate with:
ARCADIA_OPENAPI_URL=http://localhost:4000/api/openapi \
node ../lib-arcadia-core-client/scripts/sync-spec.mjs
Requires openapi-typescript in the consuming app's devDeps:
npm i -D openapi-typescript
The generated file lives at src/generated/openapi.d.ts. Until it's been generated at least once, the file is a stub and only the hand-written types in src/types.ts are useful.
Two surfaces: generic + typed
The client gives you both a generic-string API and a fully typed (OpenAPI-driven) API. They share the same auth/retry/error plumbing.
const arcadia = useArcadiaClient();
// Generic — accepts any string path. Use when the spec is incomplete or
// when calling endpoints outside the spec.
const res = await arcadia.GET<{ data: Resource[] }>(
"/api/v1/digital_objects",
{ params: { page: 1 } },
);
// Typed — paths, params, and responses inferred from the generated spec.
// Throws ArcadiaError on non-2xx.
const { data } = await arcadia.typed.GET("/api/v1/digital_objects", {
params: { query: { page: 1 } },
});
Realtime
Phoenix Channels at /socket/tenant, opt-in via the provider. The socket auto-connects when enableRealtime is on, joins tenant:<id> (and optionally tenant:<id>:user:<userId>), and disconnects on unmount.
<ArcadiaProvider
baseUrl={ARCADIA_URL}
initialTenantId={tenantId}
userId={userId}
enableRealtime
getToken={() => sessionStorage.getItem("arcadia_access_token")}
>
…
</ArcadiaProvider>
import { useArcadiaSubscription } from "@crema/arcadia-core-client";
function NotificationToasts() {
useArcadiaSubscription("notification", (n) => {
toast(n.title, n.body);
});
return null;
}
Known events on TenantEventMap: notification, digital_object, announcement, status_update, event, plus social: social:notification, social:post, social:article. The map is open-ended — apps can subscribe to any string event arcadia emits; payload type defaults to Record<string, unknown>.
For user-scoped events (those filtered to the current user), pass { scope: "user" } and ensure userId was provided to the provider.
Social bindings
For the social surfaces (profiles, articles, discussion board, favourites, DNA-change subscriptions, notifications, search, @-mention search), there's a typed wrapper over the generic client:
import { createArcadiaClient, createSocialBindings } from "@crema/arcadia-core-client";
const client = createArcadiaClient({ baseUrl, getToken });
const social = createSocialBindings(client);
await social.createPost({ body_md: "hello" });
await social.search("agent design");
await social.addSubscription(agentId, { lastSeenDnaHash: currentHash });
In a React app, prefer the useSocial() hook pattern (see
arcadia-aifirst-starter/app/lib/api/social.ts) so the bindings
pick up the ArcadiaProvider's auth context automatically.
Realtime: the social write paths broadcast via Phoenix.PubSub →
ArcadiaWeb.TenantChannel forwards them as social:notification /
social:post / social:article channel events. Subscribe via
useArcadiaSubscription.
What's not in here yet
- TanStack Query helpers — opt-in. Vibespace doesn't use Query today; we can layer it via a sub-export later.
- Token refresh helper — the client surfaces 401 via
onUnauthorized; the app decides whether to refresh + retry. A reference refresh helper may land later.
Conventions
- Inline imports only — no own
package.json(lib lives by the consuming app's deps). - Path-aliased into apps via
tsconfig.jsonpaths:@crema/arcadia-core-client→../lib-arcadia-core-client/src/index.tsx. - Tailwind doesn't scan this lib — no UI; nothing to scan.