- Define the React-specific provider + hooks exposed by the SDK.
- Document non-React helpers (server actions, job workers) that share the same interfaces.
- Ensure APIs provide concise, discriminated status enums and SSR safety.
ATProtoAuthProviderwraps children with SDK context.- Props:
sdk?: ATProtoSDK(optional; default builder if omitted)initialSession?: Session | nullchildren: React.ReactNode
- Responsibilities:
- Ensure SDK instance is memoized.
- Hydrate session state on mount, including SSR-safe guards.
- Provide event bridge so session changes propagate to all hooks.
Returns the context-bound SDK instance. Throws descriptive error if provider is missing.
function useATProtoSDK(): ATProtoSDK;Consolidated auth hook providing session data, auth status, and all auth actions.
Design Decision: Merged session management into useAuth to avoid confusion about which hook to use.
type AuthStatus = "idle" | "authorizing" | "authenticated" | "error";
interface UseAuthResult {
// Session data
session: Session | null;
status: AuthStatus;
error: Error | null;
isValid: boolean; // Whether session is valid and not expired
// Actions
login(identifier: string, redirectUrl?: string): Promise<void>;
logout(): Promise<void>;
refresh(): Promise<void>; // Force refresh session from storage/server
// Loading states
isLoading: boolean; // Any auth operation in progress
}Usage:
function AuthButton() {
const { status, session, login, logout } = useAuth();
switch (status) {
case "idle":
return <Button onClick={() => login("bsky.social")}>Sign in</Button>;
case "authorizing":
return <Button disabled>Signing in...</Button>;
case "authenticated":
return <Button onClick={logout}>Sign out ({session.handle})</Button>;
case "error":
return <Button onClick={() => login("bsky.social")}>Retry</Button>;
}
}Low-level escape hatch for direct repository access. Most apps should use domain hooks instead.
interface UseRepositoryOptions {
repoDid?: string; // Defaults to session DID
server?: "pds" | "sds"; // Force server type
serverUrl?: string; // Force specific URL (overrides server)
}
interface UseRepositoryResult {
repository: Repository | null;
status: "idle" | "loading" | "ready" | "error";
error: Error | null;
isSDS: boolean;
serverUrl: string | null;
}Profile read + write combined in a single hook. Returns Certified profiles by default, which include hypercerts-specific
fields like pronouns and website.
interface UseProfileResult {
profile: CertifiedProfile | null; // Returns Certified profile
isLoading: boolean;
error: Error | null;
update(params: UpdateCertifiedProfileParams): Promise<void>;
isUpdating: boolean;
refetch(): Promise<void>;
}
interface CertifiedProfile {
handle?: string;
displayName?: string;
description?: string;
avatar?: string; // Blob URL
banner?: string; // Blob URL
pronouns?: string; // Max 20 graphemes
website?: string;
}
interface UpdateCertifiedProfileParams {
displayName?: string | null;
description?: string | null;
avatar?: Blob | null;
banner?: Blob | null;
pronouns?: string | null;
website?: string | null;
}Usage:
function ProfileEditor() {
const { profile, update, isUpdating } = useProfile();
return (
<form onSubmit={() => update({
displayName: newName,
pronouns: newPronouns,
website: newWebsite
})}>
<input defaultValue={profile?.displayName} />
<input defaultValue={profile?.pronouns} placeholder="Pronouns" />
<input defaultValue={profile?.website} placeholder="Website" />
<button disabled={isUpdating}>Save</button>
</form>
);
}Note: The useProfile hook returns Certified profiles (app.certified.actor.profile) which include
hypercerts-specific fields. For Bluesky profiles, use the core SDK directly:
const repo = useRepository();
const bskyProfile = await repo.profile.getBskyProfile();SDS organization management with both list and singular variants.
// List + create
interface UseOrganizationsResult {
organizations: Organization[];
isLoading: boolean;
error: Error | null;
create(params: CreateOrgParams): Promise<Organization>;
isCreating: boolean;
refetch(): Promise<void>;
}
// Singular
interface UseOrganizationResult {
organization: Organization | null;
isLoading: boolean;
error: Error | null;
refetch(): Promise<void>;
}Collaborator management for SDS repositories. Throws SDSRequiredError if used on PDS.
interface UseCollaboratorsResult {
collaborators: Collaborator[];
isLoading: boolean;
error: Error | null;
grant(params: GrantParams): Promise<void>;
revoke(userDid: string): Promise<void>;
isGranting: boolean;
isRevoking: boolean;
refetch(): Promise<void>;
}Hypercert operations with both list and singular variants.
// List + create (with pagination)
interface UseHypercertsResult {
hypercerts: Hypercert[];
isLoading: boolean;
error: Error | null;
create(params: CreateHypercertParams): Promise<CreateResult>;
isCreating: boolean;
hasNextPage: boolean;
fetchNextPage(): Promise<void>;
refetch(): Promise<void>;
}
// Singular (for detail view with update/delete)
interface UseHypercertResult {
hypercert: Hypercert | null;
isLoading: boolean;
error: Error | null;
update(params: UpdateHypercertParams): Promise<void>;
remove(): Promise<void>;
isUpdating: boolean;
isDeleting: boolean;
refetch(): Promise<void>;
}Usage:
// List view
function HypercertList({ orgDid }) {
const { hypercerts, create, hasNextPage, fetchNextPage } = useHypercerts(orgDid);
// ...
}
// Detail view
function HypercertDetail({ uri }) {
const { hypercert, update, remove, isDeleting } = useHypercert(uri);
// ...
}Server Actions and background jobs don't need React hooks or special helpers - they use sdk-core directly:
// Server Action example
"use server";
import { createATProtoSDK } from "@hypercerts-org/sdk-core";
import { cookies } from "next/headers";
export async function createHypercert(data: FormData) {
const sdk = createATProtoSDK(config);
const sessionDid = cookies().get("atproto-session")?.value;
const session = await sdk.restoreSession(sessionDid);
const repo = sdk.repository(session);
return repo.hypercerts.create({ ... });
}
// Job worker example
async function processJob(userDid: string) {
const sdk = createATProtoSDK(config);
const session = await sdk.restoreSession(userDid);
// ... do work
}No special wrappers needed - the core SDK handles these contexts directly.
- Hooks export precise TypeScript types; no
any. - Provide JSDoc/TSDoc comments so Typedoc generates accurate docs.
- Keep React as an optional peer dependency to avoid bloating non-React consumers.