@archie/js-sdk
v0.1.1
Published
Archie BAAS - Core isomorphic TypeScript SDK
Readme
@archie/js-sdk
Core TypeScript SDK for the Archie BAAS platform. Isomorphic — works in both browser and Node.js.
Table of Contents
- Installation
- Quick Start
- Configuration
- Auth Module
- GraphQL Module
- Files Module
- Realtime Module
- REST Module
- Error Handling
- Retry & Timeout
- Custom Fetch
- Health Check
- Storage Adapters
- TypeScript Types
Installation
npm install @archie/js-sdk
# or
pnpm add @archie/js-sdkQuick Start
import { createClient } from '@archie/js-sdk';
const archie = createClient({
projectId: 'your-project-uuid',
apiKey: 'anon your-api-key',
apiUrl: 'https://your-project.archiecore.com',
environment: 'master',
});
// Sign in
const { session } = await archie.auth.signIn({
email: '[email protected]',
password: 'secret',
});
// Query data
const { data, error } = await archie.graphql.query('{ users { id email } }');Configuration
const archie = createClient({
// Required
projectId: 'your-project-uuid',
// API key — sent as-is in the Authorization header when no user JWT is present.
// Examples: 'anon xxxxx', 'Bearer eyJhbG...', 'custom-prefix token123'
apiKey: 'anon your-api-key',
// Environment (default: 'master')
environment: 'development',
// Base URLs
apiUrl: 'https://example.platform.com',
authUrl: 'https://example.platform.com', // defaults to apiUrl
realtimeUrl: 'https://example.platform.com', // defaults to apiUrl
// Session behavior
autoRefreshToken: true, // auto-refresh JWT before expiry (default: true)
persistSession: true, // persist session in storage (default: true)
// Custom storage adapter (default: localStorage in browser, memory in Node)
storageAdapter: customAdapter,
// Extra headers added to every request
headers: { 'x-custom': 'value' },
// Retry & timeout
timeout: 30000, // request timeout in ms (default: 30000, 0 = disabled)
retries: 3, // max retry attempts for transient errors (default: 0)
retryDelay: 200, // initial backoff delay in ms (default: 200)
// Custom fetch — for testing, proxies, or edge runtimes
fetch: customFetchImpl,
// Logging
debug: false, // enable console logging (default: false)
logger: customLogger, // custom Logger implementation (overrides debug flag)
});| Option | Type | Default | Description |
| ------------------ | ----------------------- | -------------------------- | ---------------------------------------- |
| projectId | string | required | Project UUID |
| apiKey | string | undefined | API key, sent as-is in Authorization |
| environment | string | 'master' | Environment name |
| apiUrl | string | 'https://api.archie.dev' | API Manager URL |
| authUrl | string | same as apiUrl | Auth service URL |
| realtimeUrl | string | same as apiUrl | WebSocket URL (auto-converts to ws://) |
| autoRefreshToken | boolean | true | Auto-refresh JWT 60s before expiry |
| persistSession | boolean | true | Persist session in storage |
| storageAdapter | StorageAdapter | auto-detect | Custom storage (localStorage or memory) |
| headers | Record<string,string> | {} | Extra headers on every request |
| timeout | number | 30000 | Request timeout in ms (0 = disabled) |
| retries | number | 0 | Max retry attempts for transient errors |
| retryDelay | number | 200 | Initial backoff delay in ms |
| fetch | typeof fetch | globalThis.fetch | Custom fetch for testing/edge runtimes |
| debug | boolean | false | Enable console logging |
| logger | Logger | noop | Custom logger implementation |
Authorization Priority
The SDK determines the Authorization header using this priority:
- User JWT (after
signIn) →Authorization: Bearer {jwt} - API key (from config) →
Authorization: {apiKey}(sent as-is) - Nothing → no
Authorizationheader
Auth Module
Access via archie.auth. Handles registration, login, session management, auto-refresh, and auth events.
Sign Up
const { userId, message } = await archie.auth.signUp({
email: '[email protected]',
password: 'StrongP@ss1',
firstName: 'Jane', // optional
lastName: 'Doe', // optional
roleId: 'role-uuid', // optional
});
console.log(message); // "Confirmation code sent to email"Confirm Sign Up
After the user receives a verification code by email:
const { session } = await archie.auth.confirmSignUp({
email: '[email protected]',
code: '123456',
});
// User is now signed in — session contains accessToken, refreshToken, userSign In
const { session } = await archie.auth.signIn({
email: '[email protected]',
password: 'secret',
});
console.log(session.user); // { id, email, firstName, lastName, roles }
console.log(session.accessToken); // JWT
console.log(session.expiresAt); // Unix timestamp (seconds)Sign Out
await archie.auth.signOut();
// Clears session, storage, and stops auto-refreshGet Current Session / User
const session = archie.auth.getSession(); // Session | null
const user = archie.auth.getUser(); // User | null
if (user) {
console.log(user.id, user.email, user.roles);
}Wait for Initialization
On page load, the SDK restores the session from storage asynchronously. Wait for it before checking auth state:
await archie.auth.waitForInit();
const user = archie.auth.getUser(); // now guaranteed to be loadedAuth State Changes
const unsubscribe = archie.auth.onAuthStateChange((event, session) => {
// event: 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED'
switch (event) {
case 'SIGNED_IN':
console.log('Logged in as', session.user.email);
break;
case 'SIGNED_OUT':
console.log('Logged out');
break;
case 'TOKEN_REFRESHED':
console.log('Token refreshed silently');
break;
}
});
// Stop listening
unsubscribe();Password Recovery
// Step 1: Request recovery email
const { message } = await archie.auth.recoverPassword({
email: '[email protected]',
});
// Step 2: Reset password with the code received by email
const { message } = await archie.auth.resetPassword({
email: '[email protected]',
code: '123456',
newPassword: 'NewStr0ngP@ss',
});Manual Token Refresh
Usually handled automatically when autoRefreshToken: true, but can be called manually:
const { session } = await archie.auth.refreshSession();Get JWKS
Fetch the JSON Web Key Set for server-side JWT verification:
const { keys } = await archie.auth.getJWKS();Auto-Refresh Behavior
When autoRefreshToken: true (default):
- After sign in, the SDK schedules a token refresh 60 seconds before expiry
- On refresh success, emits
TOKEN_REFRESHEDand schedules the next refresh - On refresh failure, calls
signOut()automatically
Session Persistence
When persistSession: true (default):
- Session is saved to storage after every sign in and token refresh
- On client creation, the SDK restores the session from storage
- If the stored token is expired, it attempts a refresh automatically
- Storage key format:
archie-auth-{projectId}-{environment}
GraphQL Module
Access via archie.graphql. Execute queries and mutations against the Archie API Manager's /graphql endpoint.
Query
Returns { data, error } — never throws for GraphQL-level errors:
interface User {
id: string;
email: string;
name: string;
}
const { data, error } = await archie.graphql.query<{ users: User[] }>(
'{ users { id email name } }',
);
if (error) {
console.error(error.message, error.code);
} else {
console.log(data.users);
}Query with Variables
const { data, error } = await archie.graphql.query<{ user: User }>(
`query GetUser($id: ID!) {
user(id: $id) { id email name }
}`,
{ id: 'user-123' },
);Mutation
const { data, error } = await archie.graphql.mutate<{ createUser: User }>(
`mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) { id email }
}`,
{ input: { email: '[email protected]', name: 'New User' } },
);Raw Request (Advanced)
For cases where you want the raw data and want errors to throw:
try {
const data = await archie.graphql.request<{ users: User[] }>({
query: '{ users { id email } }',
variables: {},
operationName: 'GetUsers',
});
console.log(data.users); // direct access, no { data, error } wrapper
} catch (err) {
// Throws GraphQLError on failure
}Request Options
Both query() and mutate() accept an optional third parameter:
const controller = new AbortController();
const { data } = await archie.graphql.query(
'{ users { id } }',
{},
{
headers: { 'x-custom': 'value' }, // extra headers for this request
signal: controller.signal, // AbortSignal for cancellation
},
);
// Cancel the request
controller.abort();GraphQL Response Shape
// Success case
{ data: T, error: null }
// Error case (GraphQL-level errors — partial data may exist)
{ data: T | null, error: GraphQLError }
// Network/HTTP error
{ data: null, error: ArchieError }Files Module
Access via archie.files. Upload, download, and manage files via GraphQL multipart uploads.
Upload a File
// From a File input
const input = document.querySelector<HTMLInputElement>('#fileInput');
const file = input.files[0];
const { url, fileId } = await archie.files.upload(file, {
filename: 'photo.jpg',
contentType: 'image/jpeg',
providerType: 's3', // optional — storage provider hint
onProgress: (pct) => console.log(`${pct}% uploaded`), // optional — progress callback
});
console.log(url); // CDN URL of the uploaded file
console.log(fileId); // Unique file ID for download/referenceUpload from a Blob or Uint8Array
// Blob
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const result = await archie.files.upload(blob, { filename: 'hello.txt' });
// Uint8Array (works in Node.js too)
const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
const result = await archie.files.upload(bytes, { filename: 'hello.bin' });Upload CSV
Import a CSV file into a database table:
const csvFile = document.querySelector<HTMLInputElement>('#csvInput').files[0];
const { result } = await archie.files.uploadCsv(csvFile, {
tableName: 'products',
transactionality: true, // optional — rollback all on error
limit: 1000, // optional — max rows to import
});
console.log(result.success); // boolean
console.log(result.rowsImported); // number
console.log(result.errors); // any import-level errorsDownload a File
const blob = await archie.files.download('file-id-123');
// In the browser, trigger a download:
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'my-file.pdf';
a.click();
URL.revokeObjectURL(url);Get File URL
Builds the file URL locally, no network request:
const url = archie.files.getUrl('file-id-123');
// → "https://example.platform.com/files?id=file-id-123"Realtime Module
Access via archie.realtime. WebSocket subscriptions using the graphql-transport-ws protocol with lazy connection and auto-reconnect.
Subscribe to a GraphQL Subscription
const sub = archie.realtime.subscribe<{ orderUpdated: Order }>(
`subscription {
orderUpdated { id status total }
}`,
{},
{
onData: (data) => {
console.log('Order updated:', data.orderUpdated);
},
onError: (error) => {
console.error('Subscription error:', error.message);
},
onComplete: () => {
console.log('Subscription ended');
},
},
);
// Later: stop listening
sub.unsubscribe();Subscribe with Variables
const sub = archie.realtime.subscribe(
`subscription OnUserActivity($userId: ID!) {
userActivity(userId: $userId) { action timestamp }
}`,
{ userId: 'user-123' },
{ onData: (data) => console.log(data) },
);Channel-Based API
A higher-level convenience API for table-level event subscriptions:
const channel = archie.realtime.channel('orders');
channel
.on('INSERT', (payload) => {
console.log('New order:', payload);
})
.on('UPDATE', (payload) => {
console.log('Order updated:', payload);
})
.on('DELETE', (payload) => {
console.log('Order deleted:', payload);
})
.on('*', (payload) => {
console.log('Any event:', payload);
});
// Start listening
channel.subscribe();
// Stop listening
channel.unsubscribe();Connection State
// Current state
console.log(archie.realtime.currentState);
// → 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'RECONNECTING'
// Listen for changes
const unsubscribe = archie.realtime.onConnectionStateChange((state) => {
console.log('WebSocket state:', state);
if (state === 'RECONNECTING') {
showReconnectingBanner();
}
});
unsubscribe();Auto-Reconnect
The WebSocket automatically reconnects with exponential backoff when the connection drops:
- Backoff schedule: 1s → 2s → 4s → 8s → 16s → 30s (max)
- Reconnect counter resets after a successful
connection_ack - The connection closes automatically when the last subscription is unsubscribed
- On
TOKEN_REFRESHED, the connection is re-established with the new credentials
REST Module
Access via archie.rest. For consuming custom REST APIs created via the Archie gateway. All standard headers (x-project-id, authorization, environment) are injected automatically.
GET
const products = await archie.rest.get<Product[]>('/api/products');
// With query parameters
const results = await archie.rest.get<Product[]>('/api/products', {
params: { category: 'electronics', limit: '10' },
});
// → GET /api/products?category=electronics&limit=10POST
const created = await archie.rest.post<Product>('/api/products', {
name: 'Widget',
price: 9.99,
});PUT
await archie.rest.put('/api/products/123', {
name: 'Updated Widget',
price: 12.99,
});PATCH
await archie.rest.patch('/api/products/123', { price: 14.99 });DELETE
await archie.rest.delete('/api/products/123');Request Options
All REST methods accept an options parameter:
const controller = new AbortController();
const data = await archie.rest.get('/api/products', {
headers: { 'x-custom': 'value' },
params: { page: '2' },
signal: controller.signal,
});REST Error Handling
REST methods throw ArchieError on non-2xx responses:
try {
await archie.rest.get('/api/products/missing');
} catch (err) {
if (err instanceof ArchieError) {
console.log(err.status); // 404
console.log(err.code); // 'HTTP_404'
console.log(err.message); // server-provided message
}
}Error Handling
The SDK provides a structured error hierarchy. All errors extend ArchieError.
Error Classes
| Class | When | Key Properties |
| -------------- | ------------------------------------------- | ---------------------------------------------- |
| ArchieError | Base class for all SDK errors | message, code, status, details, hint |
| AuthError | Authentication failures (401/403) | Same as ArchieError |
| GraphQLError | GraphQL response errors | path, locations + ArchieError props |
| NetworkError | Network issues (timeout, DNS, disconnected) | hint = "Check your internet..." |
Catching Errors
import { ArchieError, AuthError, GraphQLError, NetworkError } from '@archie/js-sdk';
try {
await archie.auth.signIn({ email: '[email protected]', password: 'wrong' });
} catch (err) {
if (err instanceof AuthError) {
console.log(err.code); // 'AUTH_INVALID_CREDENTIALS'
console.log(err.status); // 401
console.log(err.message); // 'Invalid credentials'
} else if (err instanceof NetworkError) {
console.log(err.hint); // 'Check your internet connection or API URL'
} else if (err instanceof ArchieError) {
console.log(err.code, err.status, err.details);
}
}GraphQL Error Handling
query() and mutate() return errors in the response instead of throwing:
const { data, error } = await archie.graphql.query('{ invalidField }');
if (error) {
console.log(error.message); // 'Cannot query field "invalidField"'
console.log(error.code); // 'GRAPHQL_VALIDATION_FAILED'
console.log(error.path); // ['invalidField']
console.log(error.locations); // [{ line: 1, column: 3 }]
}Auth Error Codes
| Code | Description |
| -------------------------- | ------------------------------------------- |
| AUTH_INVALID_CREDENTIALS | Wrong email/password |
| AUTH_EMAIL_EXISTS | Email already registered |
| AUTH_EMAIL_NOT_VERIFIED | Email confirmation required |
| AUTH_ACCOUNT_LOCKED | Account temporarily locked |
| AUTH_INVALID_CODE | Invalid verification/recovery code |
| AUTH_TOKEN_EXPIRED | Access token has expired |
| AUTH_TOKEN_INVALID | Malformed or revoked token |
| AUTH_NOT_CONFIGURED | Auth not enabled for the project |
| AUTH_WEAK_PASSWORD | Password doesn't meet strength requirements |
Automatic 401 Retry
When a request returns 401:
- The SDK attempts to refresh the token automatically
- If refresh succeeds, the original request is retried with the new token
- If refresh fails, the user is signed out and the error is thrown
This is transparent — your code doesn't need to handle it.
Retry & Timeout
The SDK supports automatic retries with exponential backoff and per-request timeouts.
Configuration
const archie = createClient({
projectId: 'your-project-uuid',
retries: 3, // retry up to 3 times on transient errors
retryDelay: 200, // initial delay: 200ms → 400ms → 800ms (with jitter)
timeout: 15_000, // abort requests after 15 seconds
});Retryable Conditions
The SDK retries automatically on:
| Condition | Status Codes / Errors |
| ---------------- | ------------------------------------- |
| Server errors | 500, 502, 503, 504 |
| Rate limiting | 429 (respects Retry-After header) |
| Network failures | TypeError (DNS, connectivity) |
| Timeouts | TIMEOUT errors |
Non-retryable errors (400, 401, 403, 404, 409, etc.) are thrown immediately.
Backoff Strategy
Exponential with jitter to prevent thundering herd:
delay = min(baseDelay × 2^(attempt-1) + random_jitter, 30s)Timeout Behavior
- When
timeout > 0, each individual request (including retries) is aborted aftertimeoutmilliseconds - Throws
ArchieErrorwithcode: 'TIMEOUT'andstatus: 408 - Set
timeout: 0to disable (no timeout) - User-provided
AbortSignaltakes precedence over timeout
Custom Fetch
Inject a custom fetch implementation for testing, proxies, or edge runtimes:
import { createClient } from '@archie/js-sdk';
// Testing with a mock
const mockFetch = vi.fn().mockResolvedValue(new Response('{}'));
const archie = createClient({
projectId: 'test',
fetch: mockFetch,
});
// Edge runtime (Cloudflare Workers, Deno)
const archie = createClient({
projectId: '...',
fetch: globalThis.fetch, // or a custom implementation
});
// Proxy / logging middleware
const archie = createClient({
projectId: '...',
fetch: async (url, init) => {
console.log('→', init?.method, url);
const response = await globalThis.fetch(url, init);
console.log('←', response.status);
return response;
},
});Health Check
Verify connectivity to the API before performing operations:
const isUp = await archie.ping();
if (isUp) {
console.log('API is reachable');
} else {
console.warn('API is unreachable');
}Returns true if the API responds with a 2xx status, false otherwise. Never throws.
Storage Adapters
The SDK uses a StorageAdapter interface for session persistence. You can provide a custom implementation.
Interface
interface StorageAdapter {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
removeItem(key: string): void | Promise<void>;
}Built-in Adapters
import { BrowserLocalStorage, MemoryStorage } from '@archie/js-sdk';
// Browser — uses localStorage (default in browser)
const archie = createClient({
projectId: '...',
storageAdapter: new BrowserLocalStorage(),
});
// In-memory — for SSR, tests, or environments without localStorage
const archie = createClient({
projectId: '...',
storageAdapter: new MemoryStorage(),
});Custom Adapter Example (AsyncStorage for React Native)
import AsyncStorage from '@react-native-async-storage/async-storage';
const archie = createClient({
projectId: '...',
storageAdapter: {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
},
});TypeScript Types
All types are exported for strong typing in your application.
Core Types
import type { ArchieClientOptions, Session, User } from '@archie/js-sdk';Auth Types
import type {
AuthSignUpParams,
AuthSignInParams,
AuthConfirmParams,
AuthRecoverParams,
AuthResetPasswordParams,
AuthEvent, // 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED'
AuthEventCallback,
} from '@archie/js-sdk';GraphQL Types
import type {
GraphQLResponse, // { data: T | null; error: ArchieError | null }
GraphQLRequestOptions, // { headers?, signal? }
GraphQLRawRequest, // { query, variables?, operationName? }
} from '@archie/js-sdk';File Types
import type {
FileUploadOptions, // { filename?, contentType?, providerType?, onProgress? }
FileUploadResult, // { url: string; fileId: string }
CsvUploadOptions, // { tableName, transactionality?, limit? }
} from '@archie/js-sdk';Realtime Types
import type {
Subscription, // { unsubscribe: () => void }
SubscriptionCallbacks, // { onData, onError?, onComplete? }
RealtimeEvent, // 'INSERT' | 'UPDATE' | 'DELETE' | '*'
RealtimeChannel, // { on, subscribe, unsubscribe }
ConnectionState, // 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'RECONNECTING'
ConnectionStateCallback,
} from '@archie/js-sdk';REST Types
import type {
RestRequestOptions, // { headers?, params?, signal? }
} from '@archie/js-sdk';Module Interfaces (for DI / Testing)
import type {
IAuthModule,
IGraphQLModule,
IFileModule,
IRealtimeModule,
IRestModule,
IHttpClient,
Logger,
TokenAccessor,
StorageAdapter,
} from '@archie/js-sdk';Module Classes (for advanced typing)
The concrete module implementations are also exported for instanceof checks or advanced use:
import { AuthModule, GraphQLModule, FileModule, RealtimeModule, RestModule } from '@archie/js-sdk';Built-in Loggers
Two logger constants are available for the logger option:
import { consoleLogger, noopLogger } from '@archie/js-sdk';
// Console logger — logs to console.debug/info/warn/error
const archie = createClient({ projectId: '...', logger: consoleLogger });
// Noop logger — silences all logging (default)
const archie = createClient({ projectId: '...', logger: noopLogger });