@raulbellosom/atlas-sdk
@raulbellosom/atlas-sdk
Generic JavaScript client for AtlasERP storefront APIs. Works in any browser environment, React app, or Vite project. Handles authentication, session persistence, file uploads, product catalog, module discovery, real-time events, and guest live chat.
# npm
npm install @raulbellosom/atlas-sdk
# pnpm
pnpm add @raulbellosom/atlas-sdkTable of contents
- Quick Start — Plain JS
- Quick Start — React + Vite
- Configuration reference
- sdk.auth
- sdk.files
- sdk.catalog
- sdk.discovery —
blueprints,modules,hasModule,introspect - sdk.realtime
- sdk.request — generic escape hatch
- StorefrontError — complete error reference
- React Hooks — complete reference
- Auth patterns
- Common patterns for module-specific data
- Environment variables for Vite projects
- Roles system
- Deploying to Atlas Website (source_type=dist)
- sdk.guestChat — guest live chat
Glossary
Company slug — a short identifier string (e.g. "acme") that tells the API which tenant you are operating on. The platform admin provides this value. Every request sends it as the X-Atlas-Company header automatically.
Blueprint — a JSON schema object that describes a data entity installed in the ERP (its fields, relations, and display metadata). Blueprints are how you discover which modules and data types are available on a given ERP instance.
Storefront role — a role scoped to public-facing access. The two built-in roles are storefront_client (end customer, 5 MB file limit, images only) and storefront_vendor (seller/partner, 100 MB file limit, all file types). The set of registrable roles is configured by the platform admin.
Session — an object with shape { user, token, refreshToken, expiresAt } held in memory by the SDK. Session persistence to localStorage is handled automatically by @supabase/supabase-js — no initialSession or manual localStorage code needed.
1. Quick Start — Plain JS
import { createStorefrontClient } from '@raulbellosom/atlas-sdk'
// Read from window.ATLAS_CONFIG (injected by Atlas Website) or env vars for local dev.
const cfg = (typeof window !== 'undefined' && window.ATLAS_CONFIG) ? window.ATLAS_CONFIG : {}
const sdk = createStorefrontClient({
baseUrl: cfg.apiUrl ?? 'https://erp.tudominio.mx',
company: cfg.company ?? 'tu-empresa',
supabaseUrl: cfg.supabaseUrl ?? 'https://supabase.tudominio.mx',
supabaseAnonKey: cfg.supabaseAnonKey ?? '<anon-key>',
})
// Log in — Supabase stores the session in localStorage automatically.
// The same session is shared with Atlas ERP.
const { user, token } = await sdk.auth.login({
email: 'cliente@ejemplo.mx',
password: 'contraseña123',
})
console.log('Bienvenido,', user?.displayName ?? 'usuario')
// Redirect ERP users to the ERP app
if (user?.hasErpAccess) {
window.location.href = cfg.apiUrl ?? 'https://erp.tudominio.mx'
}
// Make an authenticated request (token is added automatically)
const { data: bookings } = await sdk.request('GET', '/public/bookings')
console.log('Reservaciones:', bookings)
// Log out
await sdk.auth.logout()The returned SDK object is frozen: { auth, files, catalog, discovery, realtime, analytics, forms, request }. Each namespace is documented in its own section below.
2. Quick Start — React + Vite
src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { createStorefrontClient } from '@raulbellosom/atlas-sdk'
import { StorefrontProvider } from '@raulbellosom/atlas-sdk/react'
import App from './App.jsx'
const cfg = (typeof window !== 'undefined' && window.ATLAS_CONFIG) ? window.ATLAS_CONFIG : {}
const sdk = createStorefrontClient({
baseUrl: cfg.apiUrl ?? import.meta.env.VITE_ERP_URL,
company: cfg.company ?? import.meta.env.VITE_ERP_COMPANY,
supabaseUrl: cfg.supabaseUrl ?? import.meta.env.VITE_SUPABASE_URL,
supabaseAnonKey: cfg.supabaseAnonKey ?? import.meta.env.VITE_SUPABASE_ANON_KEY,
})
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<StorefrontProvider client={sdk}>
<App />
</StorefrontProvider>
</React.StrictMode>
)src/components/LoginForm.jsx
import { useState } from 'react'
import { useAuth } from '@raulbellosom/atlas-sdk/react'
import { StorefrontError } from '@raulbellosom/atlas-sdk'
export function LoginForm({ onSuccess }) {
const { login, isLoading, error } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await login({ email, password })
onSuccess()
} catch (err) {
// error is already set on the hook; no action needed here
}
}
return (
<form onSubmit={handleSubmit}>
<h2>Iniciar sesion</h2>
{error && (
<p style={{ color: 'red' }}>
{error instanceof StorefrontError
? error.message
: 'Ocurrio un error inesperado'}
</p>
)}
<label>
Correo electronico
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
<label>
Contrasena
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Entrando...' : 'Entrar'}
</button>
</form>
)
}src/components/ProductList.jsx
import { useProducts } from '@raulbellosom/atlas-sdk/react'
export function ProductList() {
const { data, isLoading, error } = useProducts({ limit: 20, page: 1 })
if (isLoading) return <p>Cargando productos...</p>
if (error) return <p>Error al cargar productos: {error.message}</p>
if (!data?.data?.length) return <p>No hay productos disponibles.</p>
return (
<ul>
{data.data.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong> — ${product.price}
</li>
))}
</ul>
)
}3. Configuration reference
createStorefrontClient(options) accepts the following options:
| Option | Type | Required | Description | Example |
|---|---|---|---|---|
baseUrl |
string |
Yes | Full URL of the ERP instance, no trailing slash | 'https://erp.tudominio.mx' |
company |
string |
Yes | Company slug assigned by the platform admin. Sent as X-Atlas-Company on every request |
'tu-empresa' |
siteId |
string |
No | Website site UUID for analytics and forms. In Atlas Website it is inferred from window.ATLAS_CONFIG.siteId |
'019...' |
supabaseUrl |
string |
Yes | Supabase project URL. Available in window.ATLAS_CONFIG.supabaseUrl for Atlas Website dists |
'https://supabase.tudominio.mx' |
supabaseAnonKey |
string |
Yes | Supabase anon key. Available in window.ATLAS_CONFIG.supabaseAnonKey |
'eyJ...' |
onSessionChange |
function(session | null) |
No | Called on every auth state change. Session is persisted by Supabase automatically — this is optional and mainly useful for debugging or syncing external state | (s) => console.log('session:', s) |
The function throws a plain Error (not a StorefrontError) synchronously if baseUrl, company, supabaseUrl, or supabaseAnonKey is missing.
4. sdk.auth
sdk.auth.register(params)
Description: Creates a new storefront user account. Does not log the user in — call sdk.auth.login after registration if you want to start a session immediately.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
email |
string |
Yes | User's email address |
password |
string |
Yes | Plain-text password (transmitted over HTTPS, hashed server-side) |
name |
string |
Yes | Display name for the user |
role |
string |
No | Storefront role to assign. Defaults to 'storefront_client'. The allowed values depend on which roles the platform admin has made registrable |
Returns: Promise<{ id, displayName, firstName, lastName, email, phone, bio, role, hasErpAccess }>
| Field | Type | Description |
|---|---|---|
id |
string |
UUID v7 of the new user record |
displayName |
string |
Full display name |
firstName |
string |
First name extracted from name |
lastName |
string |
Last name extracted from name |
email |
string |
Email address |
phone |
string | null |
Phone number (null until set by the user) |
bio |
string | null |
Short bio (null until set by the user) |
role |
string |
Assigned storefront role |
hasErpAccess |
boolean |
true if the user's role has the platform.erp.access permission (typically false for storefront roles) |
Example:
import { StorefrontError } from '@raulbellosom/atlas-sdk'
try {
const user = await sdk.auth.register({
email: 'nuevo@ejemplo.mx',
password: 'MiContrasena456',
name: 'Ana Garcia',
role: 'storefront_client',
})
console.log('Cuenta creada:', user.displayName)
} catch (err) {
if (err instanceof StorefrontError) {
if (err.code === 'VALIDATION_ERROR') {
console.error('Datos invalidos:', err.details)
} else if (err.status === 409) {
console.error('Este correo ya esta registrado')
} else {
console.error(err.message)
}
}
}Errors thrown:
VALIDATION_ERROR(422) — missing or invalid fields;err.detailscontains per-field messagesUNKNOWN(409) — email already registeredNETWORK_ERROR(0) — could not reach the server
sdk.auth.login(params)
Description: Authenticates with email and password, stores the session internally, and calls onSessionChange if configured.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
email |
string |
Yes | Registered email address |
password |
string |
Yes | Account password |
Returns: Promise<{ user, token, refreshToken, expiresAt }>
| Field | Type | Description |
|---|---|---|
user |
object |
{ id, displayName, firstName, lastName, email, phone, bio, role, hasErpAccess } |
token |
string |
JWT access token (short-lived) |
refreshToken |
string |
Opaque refresh token (long-lived) |
expiresAt |
string |
ISO 8601 datetime when token expires |
Example:
import { StorefrontError } from '@raulbellosom/atlas-sdk'
try {
const session = await sdk.auth.login({
email: 'cliente@ejemplo.mx',
password: 'MiContrasena456',
})
console.log('Bienvenido,', session.user.displayName)
console.log('Token expira:', session.expiresAt)
} catch (err) {
if (err instanceof StorefrontError) {
if (err.code === 'UNAUTHORIZED') {
console.error('Correo o contrasena incorrectos')
} else {
console.error(err.message)
}
}
}Errors thrown:
UNAUTHORIZED(401) — wrong email or passwordNETWORK_ERROR(0) — could not reach the server
sdk.auth.me()
Description: Returns the profile of the currently authenticated user. Requires an active session.
Parameters: None
Returns: Promise<{ id, displayName, firstName, lastName, email, phone, bio, role, hasErpAccess }>
| Field | Type | Description |
|---|---|---|
id |
string |
UUID v7 of the user record |
displayName |
string |
Full display name |
firstName |
string |
First name |
lastName |
string |
Last name |
email |
string |
Email address |
phone |
string | null |
Phone number |
bio |
string | null |
Short bio |
role |
string |
Assigned role key |
hasErpAccess |
boolean |
true if the user's role has the platform.erp.access permission |
Example:
const profile = await sdk.auth.me()
console.log('Perfil:', profile.email, profile.role)
if (profile.hasErpAccess) {
window.location.href = window.ATLAS_CONFIG?.apiUrl ?? '/'
}Errors thrown:
UNAUTHORIZED(401) — no active session or expired token that could not be refreshed
sdk.auth.refresh()
Description: Exchanges the stored refreshToken for a new token and refreshToken pair. You normally do not need to call this manually — the SDK calls it automatically when any request returns 401. Exposed for manual use if needed.
Parameters: None
Returns: Promise<{ token, refreshToken, expiresAt }>
Example:
try {
const tokens = await sdk.auth.refresh()
console.log('Token renovado, expira:', tokens.expiresAt)
} catch (err) {
// Refresh failed — session is cleared automatically, user must log in again
console.error('Sesion expirada, inicia sesion de nuevo')
}Errors thrown:
- Plain
Error— if there is no active session with arefreshToken UNAUTHORIZED(401) — if the refresh token is invalid or expired (session is cleared)
sdk.auth.logout()
Description: Invalidates the session server-side and clears the in-memory session, triggering onSessionChange(null).
Parameters: None
Returns: Promise<void>
Example:
await sdk.auth.logout()
// Session is now null. onSessionChange was called with null.
// Supabase clears its localStorage key automatically via the SIGNED_OUT event.Errors thrown:
NETWORK_ERROR(0) — if the server is unreachable (the local session is still cleared)
sdk.auth.getSession()
Description: Synchronously returns the current in-memory session without making a network request.
Parameters: None
Returns: { user, token, refreshToken, expiresAt } | null
Example:
const session = sdk.auth.getSession()
if (session) {
console.log('Sesion activa para:', session.user.email)
} else {
console.log('Sin sesion activa')
}sdk.auth.onAuthStateChange(fn)
Description: Subscribes to session changes. The callback is called every time the session is set (login or token refresh) or cleared (logout or refresh failure). Returns an unsubscribe function.
Parameters:
| Name | Type | Description |
|---|---|---|
fn |
function(session | null) |
Called with the full session object, or null when the session is cleared |
Returns: function — call this to unsubscribe
Example:
const unsubscribe = sdk.auth.onAuthStateChange((session) => {
if (session) {
console.log('Usuario autenticado:', session.user.email)
} else {
console.log('Sesion cerrada')
window.location.href = '/login'
}
})
// Later, when the listener is no longer needed:
unsubscribe()5. sdk.files
Files uploaded via the SDK are stored in Supabase Storage. Access control and size limits depend on the authenticated user's role.
Role-based limits:
| Role | Max file size | Allowed types |
|---|---|---|
storefront_client |
5 MB | Images only (image/*) |
storefront_vendor |
100 MB | All file types |
sdk.files.upload(file, options?)
Description: Uploads a File or Blob to Supabase Storage. Requires an authenticated session.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
file |
File | Blob |
Yes | The file object to upload (from <input type="file"> or a Blob) |
options.visibility |
'PUBLIC' | 'PRIVATE' |
No | Whether the file is publicly accessible. Defaults to 'PUBLIC' |
options.entityType |
string |
No | Optional label identifying what kind of record this file belongs to (e.g. 'product', 'booking') |
options.entityId |
string |
No | Optional UUID of the specific record this file belongs to |
Returns: Promise<{ id, url, originalName, mimeType, sizeBytes }>
| Field | Type | Description |
|---|---|---|
id |
string |
UUID v7 of the created FileAsset record |
url |
string |
Permanent URL if visibility is PUBLIC; signed URL (1 hour) if PRIVATE |
originalName |
string |
Original file name as uploaded |
mimeType |
string |
MIME type of the file |
sizeBytes |
number |
File size in bytes |
Example:
const fileInput = document.getElementById('foto')
const file = fileInput.files[0]
try {
const asset = await sdk.files.upload(file, {
visibility: 'PUBLIC',
entityType: 'product',
entityId: '01933b7e-0000-7000-8000-000000000001',
})
console.log('Archivo subido:', asset.url)
} catch (err) {
if (err.code === 'VALIDATION_ERROR') {
console.error('Tipo de archivo no permitido o tamano excedido')
} else if (err.code === 'UNAUTHORIZED') {
console.error('Debes iniciar sesion para subir archivos')
}
}sdk.files.getUrl(id)
Description: Returns the URL for a file asset. For PUBLIC files this is a permanent URL. For PRIVATE files this is a signed URL valid for 1 hour.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | The FileAsset UUID returned by upload() |
Returns: Promise<{ url?: string, signedUrl?: string, type: 'public' | 'signed' }>
| Field | Type | Description |
|---|---|---|
url |
string |
Present when type is 'public'. Permanent URL. |
signedUrl |
string |
Present when type is 'signed'. Expires in 1 hour. |
type |
'public' | 'signed' |
Indicates which field contains the URL |
Example:
const result = await sdk.files.getUrl('01933b7e-0000-7000-8000-000000000001')
const displayUrl = result.type === 'public' ? result.url : result.signedUrl
document.getElementById('imagen').src = displayUrlsdk.files.getSignedUrl(id)
Description: Alias for getUrl. Returns the same response. Use when you specifically want to signal intent to get a signed (temporary) URL, though the server decides based on visibility.
Parameters: Same as getUrl.
Returns: Same as getUrl.
sdk.files.delete(id)
Description: Deletes a file asset. The caller must be the user who uploaded the file.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | The FileAsset UUID |
Returns: Promise<{ success: true }>
Example:
try {
await sdk.files.delete('01933b7e-0000-7000-8000-000000000001')
console.log('Archivo eliminado')
} catch (err) {
if (err.code === 'FORBIDDEN') {
console.error('No tienes permiso para eliminar este archivo')
} else if (err.code === 'NOT_FOUND') {
console.error('El archivo no existe')
}
}6. sdk.catalog
sdk.catalog.products(options?)
Description: Lists products from the catalog. Returns the raw API response envelope (not just the data array) so pagination metadata is accessible.
Parameters (all optional):
| Name | Type | Description |
|---|---|---|
page |
number |
Page number, 1-based. Defaults to 1 |
limit |
number |
Items per page. Defaults to server default (typically 20) |
search |
string |
Full-text search term |
categoryId |
string |
Filter by category UUID |
sort |
string |
Sort field, e.g. 'name', 'price' |
order |
'asc' | 'desc' |
Sort direction |
Returns: Promise<{ data: Array<product>, total: number, page: number, limit: number }>
Each product object has at minimum { id, name, price }. Additional fields depend on the ERP configuration and which blueprints are installed.
Example:
const result = await sdk.catalog.products({ page: 1, limit: 10, search: 'zapato' })
console.log(`Mostrando ${result.data.length} de ${result.total} productos`)
for (const product of result.data) {
console.log(product.name, product.price)
}sdk.catalog.getProduct(id)
Description: Returns a single product by its UUID.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Product UUID |
Returns: Promise<product> — a single product object
Example:
try {
const product = await sdk.catalog.getProduct('01933b7e-0000-7000-8000-000000000002')
console.log(product.name, product.price)
} catch (err) {
if (err.code === 'NOT_FOUND') {
console.error('Producto no encontrado')
}
}sdk.catalog.categories(options?)
Description: Lists product categories. Returns the raw API response envelope.
Parameters (all optional):
| Name | Type | Description |
|---|---|---|
page |
number |
Page number, 1-based |
limit |
number |
Items per page |
Returns: Promise<{ data: Array<category>, total: number, page: number, limit: number }>
Each category object has at minimum { id, name }.
Example:
const result = await sdk.catalog.categories()
for (const category of result.data) {
console.log(category.id, category.name)
}7. sdk.discovery
The discovery namespace lets you query which modules (features) are installed and enabled on the ERP instance, what data structures they expose, and how to reach their endpoints.
Caching: All results are cached in memory for 30 seconds. Concurrent calls while a fetch is in-flight share a single promise — the network request is made only once. The cache resets on client instantiation.
Quick discovery via GET /public: Any running Atlas ERP instance exposes a meta-endpoint at GET /public (no auth required) that lists every available public endpoint with its method, path, auth requirement, and description. This is the fastest way for a developer, tool, or AI agent to discover what the instance supports without reading documentation:
curl https://erp.tudominio.mx/publicWhat is a blueprint? A blueprint is a JSON schema object describing a data view registered by an installed ERP module (e.g. a custom screen component, a public entity view). Each blueprint has at minimum: { key, moduleKey, kind, schema }.
What is a module? A module is an installable ERP feature (e.g. atlas.catalog, custom.bookings). The modules() method returns the list of modules currently installed and enabled on the running instance, including their navigation structure and what they expose publicly.
sdk.discovery.modules()
Description: Returns all installed and enabled modules on this ERP instance. Results are cached for 30 seconds. Use this to know which features are active before building module-specific UI or making module-specific API calls.
Parameters: None
Returns: Promise<Array<module>>
Each module object:
| Field | Type | Description |
|---|---|---|
key |
string |
Unique module identifier, e.g. 'atlas.catalog', 'custom.bookings' |
name |
string |
Human-readable module name |
version |
string |
Installed version string |
kind |
string |
Module kind: 'CORE', 'FEATURE', or 'CUSTOM' |
enabled |
boolean |
Always true in this response (only enabled modules are returned) |
navigation |
Array |
Navigation items declared by this module (label, path, icon) |
exposes |
Array |
Public routes or capabilities this module exposes |
Example:
const mods = await sdk.discovery.modules()
console.log(`Modulos activos: ${mods.length}`)
for (const mod of mods) {
console.log(`${mod.key} v${mod.version} — ${mod.name}`)
}
// Check if a specific module is active
const hasBookings = mods.some(m => m.key === 'custom.bookings')
if (hasBookings) {
// Show bookings UI
}sdk.discovery.blueprints()
Description: Returns all public custom views (blueprints) registered by enabled modules on this ERP instance. Results are cached for 30 seconds. Blueprints describe UI components that modules declare as publicly renderable.
Parameters: None
Returns: Promise<Array<blueprint>>
Each blueprint object includes: { key, moduleKey, kind, schema: { component, path, title, public } }.
Example:
const blueprints = await sdk.discovery.blueprints()
console.log(`Vistas publicas registradas: ${blueprints.length}`)
const bookingViews = blueprints.filter(bp => bp.moduleKey === 'custom.bookings')
console.log('Vistas de reservaciones:', bookingViews.map(bp => bp.key))sdk.discovery.hasModule(moduleKey)
Description: Returns true if the given module key is installed and enabled on this ERP instance. Uses the cached modules() result.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
moduleKey |
string |
Yes | The module identifier, e.g. 'custom.bookings', 'atlas.catalog' |
Returns: Promise<boolean>
Example:
const hasCatalog = await sdk.discovery.hasModule('atlas.catalog')
if (!hasCatalog) {
console.warn('El modulo de catalogo no esta disponible en este servidor')
}sdk.discovery.introspect()
Description: Fetches modules and blueprints in parallel and returns a unified schema object. This is the primary method for an external project or AI agent to build a complete picture of what an Atlas ERP instance supports — which modules are active, what they expose, and what views they declare.
Parameters: None
Returns: Promise<{ modules, blueprints, byModuleKey }>
| Field | Type | Description |
|---|---|---|
modules |
Array<module> |
Same as sdk.discovery.modules() |
blueprints |
Array<blueprint> |
Same as sdk.discovery.blueprints() |
byModuleKey |
object |
Each key is a module key; each value is the module object extended with a blueprints array containing that module's views |
Example:
const { modules, blueprints, byModuleKey } = await sdk.discovery.introspect()
console.log(`Modulos activos: ${modules.length}`)
console.log(`Vistas publicas: ${blueprints.length}`)
// Access a specific module and all its views
const catalog = byModuleKey['atlas.catalog']
if (catalog) {
console.log(`Catalogo v${catalog.version}, vistas: ${catalog.blueprints.length}`)
console.log('Navegacion:', catalog.navigation.map(n => n.label))
}
// Enumerate everything — useful for AI agents or integration tools
for (const [key, mod] of Object.entries(byModuleKey)) {
console.log(`${key}: ${mod.blueprints.length} vistas`)
}Recommended use case — AI agent or integration bootstrap:
// Call once at startup to understand what this ERP instance supports
const schema = await sdk.discovery.introspect()
// Now you can make informed decisions:
if (schema.byModuleKey['custom.reservations']) {
loadReservationsModule(schema.byModuleKey['custom.reservations'])
}
if (schema.byModuleKey['atlas.catalog']) {
loadCatalogModule(schema.byModuleKey['atlas.catalog'])
}8. sdk.realtime
The realtime namespace provides live database-change events via Supabase Realtime. It is scoped to the company that the SDK was initialized with.
Lazy connection: The Supabase WebSocket connection is established only on the first on() call. Importing the SDK or calling other namespaces does not open a WebSocket.
Event key format: "<tableName>.<eventType>" where eventType is one of insert, update, or delete (lowercase). Example: "reservaciones.insert".
sdk.realtime.on(event, handler)
Description: Subscribes to a real-time database event. Opens the Supabase connection if not already open.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
event |
string |
Yes | Event key in "table.eventtype" format, e.g. 'reservaciones.update' |
handler |
function(row) |
Yes | Called with the new row (for insert/update) or the deleted row (for delete) when the event fires |
Returns: void
sdk.realtime.off(event, handler)
Description: Removes a previously registered handler. The WebSocket connection stays open until dispose() is called.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
event |
string |
Yes | Same event key passed to on() |
handler |
function |
Yes | The exact same function reference passed to on() |
Returns: void
sdk.realtime.dispose()
Description: Removes the Supabase channel subscription and closes the WebSocket connection. Clears all handlers. Call this when tearing down the application or when you no longer need live events.
Parameters: None
Returns: Promise<void>
Complete example — booking status subscription:
// Subscribe to status changes on a bookings table
function handleBookingUpdate(row) {
console.log('Reservacion actualizada:', row.id, 'Estado:', row.status)
if (row.status === 'CONFIRMED') {
showNotification('Tu reservacion ha sido confirmada')
}
}
sdk.realtime.on('reservaciones.update', handleBookingUpdate)
// When navigating away or unmounting
function cleanup() {
sdk.realtime.off('reservaciones.update', handleBookingUpdate)
}
// When the user logs out or the app is closed
async function shutdown() {
await sdk.realtime.dispose()
}9. sdk.request — generic escape hatch
sdk.request(method, path, body?, options?) lets you call any API endpoint that does not have a dedicated SDK namespace. This is the primary way to interact with custom ERP modules (bookings, reviews, loyalty points, etc.) before they receive a dedicated SDK namespace.
The method automatically:
- Attaches the
Authorization: Bearer <token>header from the current session - Attaches the
X-Atlas-Companyheader - Retries once with a refreshed token on 401 responses
- Throws
StorefrontErroron non-2xx responses
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
method |
string |
Yes | HTTP method: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' |
path |
string |
Yes | API path starting with /, e.g. '/public/bookings' |
body |
object | FormData | null |
No | Request body. Objects are JSON-serialized. FormData is sent as multipart. Omit or pass null for GET/DELETE |
options |
object |
No | Additional options. Supports options.headers (extra headers as a plain object) |
Returns: Promise<any> — the parsed JSON response from the API
Example — list bookings:
const response = await sdk.request('GET', '/public/bookings')
const bookings = response.dataExample — create a booking:
const response = await sdk.request('POST', '/public/bookings', {
serviceId: '01933b7e-0000-7000-8000-000000000003',
date: '2026-07-15',
time: '10:00',
notes: 'Sin gluten por favor',
})
const newBooking = response.data
console.log('Reservacion creada:', newBooking.id)Example — cancel a booking:
await sdk.request('PATCH', `/public/bookings/${bookingId}`, {
status: 'CANCELLED',
})10. StorefrontError — complete error reference
All SDK methods throw StorefrontError on failure (except createStorefrontClient which throws a plain Error for missing config).
import { StorefrontError } from '@raulbellosom/atlas-sdk'Class properties:
| Property | Type | Description |
|---|---|---|
message |
string |
Human-readable error message from the server, or a generic fallback |
code |
string |
Machine-readable error code (see table below) |
status |
number |
HTTP status code. 0 for network errors (server unreachable) |
details |
any | null |
Extra information, typically per-field validation errors as an object |
name |
string |
Always 'StorefrontError' |
Error codes:
| Code | HTTP Status | Meaning | When thrown |
|---|---|---|---|
UNAUTHORIZED |
401 | Not authenticated or token expired | Calling a protected endpoint without a session, or after a failed token refresh |
FORBIDDEN |
403 | Authenticated but not allowed | Trying to delete someone else's file, accessing an admin-only endpoint |
NOT_FOUND |
404 | Resource does not exist | getProduct with a non-existent ID, getUrl with an unknown file ID |
VALIDATION_ERROR |
422 | Request body failed server-side validation | Missing required fields, invalid email format, file type not allowed |
MODULE_NOT_AVAILABLE |
— | Module required for this operation is not installed | Calling catalog endpoints when atlas.catalog is not installed |
NETWORK_ERROR |
0 | Could not reach the server at all | No internet connection, DNS failure, server down |
UNKNOWN |
varies | Any other non-2xx response | Server errors (500), conflicts (409), rate limiting (429) |
try/catch pattern:
import { StorefrontError } from '@raulbellosom/atlas-sdk'
try {
const session = await sdk.auth.login({ email, password })
// success
} catch (err) {
if (err instanceof StorefrontError) {
switch (err.code) {
case 'UNAUTHORIZED':
setError('Correo o contrasena incorrectos')
break
case 'VALIDATION_ERROR':
// err.details may be an object like { email: 'Formato invalido' }
setError('Revisa los campos del formulario')
setFieldErrors(err.details)
break
case 'NETWORK_ERROR':
setError('Sin conexion. Verifica tu internet e intenta de nuevo')
break
default:
setError('Ocurrio un error inesperado. Intenta de nuevo.')
}
} else {
// Programming error — rethrow
throw err
}
}11. React Hooks — complete reference
All hooks are exported from @raulbellosom/atlas-sdk/react. They must be used inside a component tree wrapped with <StorefrontProvider>.
StorefrontProvider + useStorefront
Import: import { StorefrontProvider, useStorefront } from '@raulbellosom/atlas-sdk/react'
StorefrontProvider is a React context provider. It makes the SDK client available to all hooks in its subtree. Place it at the root of your application.
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
client |
SDK client | Yes | The object returned by createStorefrontClient(...) |
children |
ReactNode |
Yes | Your application tree |
useStorefront() returns the SDK client directly. Use this if you need to call SDK methods outside of the built-in hooks (e.g. sdk.realtime.on). Throws if called outside of StorefrontProvider.
Example:
import { useEffect } from 'react'
import { useStorefront } from '@raulbellosom/atlas-sdk/react'
function RealtimeListener() {
const sdk = useStorefront()
useEffect(() => {
function handleUpdate(row) {
console.log('Fila actualizada:', row)
}
sdk.realtime.on('pedidos.update', handleUpdate)
return () => {
sdk.realtime.off('pedidos.update', handleUpdate)
}
}, [sdk])
return null
}useSession()
Import: import { useSession } from '@raulbellosom/atlas-sdk/react'
Signature: useSession(): { user, token, refreshToken, expiresAt } | null
Description: Returns the current session and re-renders the component whenever the session changes (login, logout, token refresh). Returns null when no session is active.
Return value:
| Field | Type | Description |
|---|---|---|
user |
object |
{ id, displayName, firstName, lastName, email, phone, bio, role, hasErpAccess } |
token |
string |
Current JWT access token |
refreshToken |
string |
Current refresh token |
expiresAt |
string |
ISO 8601 expiry datetime of the access token |
Returns null when no user is logged in.
Example:
import { useSession } from '@raulbellosom/atlas-sdk/react'
function UserBadge() {
const session = useSession()
if (!session) {
return <span>Visitante</span>
}
return <span>Hola, {session.user.displayName}</span>
}useAuth()
Import: import { useAuth } from '@raulbellosom/atlas-sdk/react'
Signature: useAuth(): { user, session, isAuthenticated, isLoading, error, login, register, logout, refresh }
Return value:
| Field | Type | Description |
|---|---|---|
user |
object | null |
Current user's profile, or null if not logged in |
session |
object | null |
Full session object (same as useSession()), or null |
isAuthenticated |
boolean |
true when a session is active |
isLoading |
boolean |
true while any auth operation (login, register, logout, refresh) is in progress |
error |
StorefrontError | null |
The last error from any auth operation, or null |
login |
async function({ email, password }) |
Calls sdk.auth.login. Updates isLoading and error |
register |
async function({ email, password, name, role? }) |
Calls sdk.auth.register. Updates isLoading and error |
logout |
async function() |
Calls sdk.auth.logout. Updates isLoading and error |
refresh |
async function() |
Calls sdk.auth.refresh. Updates isLoading and error |
All four action functions set isLoading to true while running, clear error at the start, and set error if the call fails. They also re-throw the error so you can catch it in the calling component.
Example:
import { useState } from 'react'
import { useAuth } from '@raulbellosom/atlas-sdk/react'
import { StorefrontError } from '@raulbellosom/atlas-sdk'
function LoginForm({ onSuccess }) {
const { login, isLoading, error } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await login({ email, password })
onSuccess()
} catch (err) {
// error state is already set on the hook; display it via the `error` field
}
}
return (
<form onSubmit={handleSubmit}>
{error && (
<div role="alert" style={{ color: 'red' }}>
{error instanceof StorefrontError && error.code === 'UNAUTHORIZED'
? 'Correo o contrasena incorrectos'
: 'Ocurrio un error. Intenta de nuevo.'}
</div>
)}
<input
type="email"
placeholder="Correo electronico"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Contrasena"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Iniciando sesion...' : 'Iniciar sesion'}
</button>
</form>
)
}useFileUpload()
Import: import { useFileUpload } from '@raulbellosom/atlas-sdk/react'
Signature: useFileUpload(): { upload, uploading, error, result, reset }
Return value:
| Field | Type | Description |
|---|---|---|
upload |
async function(file, options?) |
Calls sdk.files.upload. Same signature and options as the plain JS method |
uploading |
boolean |
true while an upload is in progress |
error |
StorefrontError | null |
Error from the last upload attempt, or null |
result |
{ id, url, originalName, mimeType, sizeBytes } | null |
Result of the last successful upload, or null |
reset |
function() |
Clears error and result back to null |
Example:
import { useRef } from 'react'
import { useFileUpload } from '@raulbellosom/atlas-sdk/react'
function AvatarUploader({ onUploaded }) {
const { upload, uploading, error, result, reset } = useFileUpload()
const inputRef = useRef(null)
async function handleChange(e) {
const file = e.target.files[0]
if (!file) return
try {
const asset = await upload(file, { visibility: 'PUBLIC', entityType: 'avatar' })
onUploaded(asset.url)
} catch (err) {
// error state is set on the hook
}
}
return (
<div>
<input ref={inputRef} type="file" accept="image/*" onChange={handleChange} />
{uploading && <p>Subiendo imagen...</p>}
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
{result && <img src={result.url} alt="Avatar subido" width={100} />}
{(error || result) && (
<button onClick={reset}>Limpiar</button>
)}
</div>
)
}useCompanyConfig()
Import: import { useCompanyConfig } from '@raulbellosom/atlas-sdk/react'
Signature: useCompanyConfig(): { config, isLoading, error }
Description: Fetches the public storefront configuration for this company. The config object typically contains branding, contact info, and feature flags set by the platform admin.
Return value:
| Field | Type | Description |
|---|---|---|
config |
object | null |
The company's storefront config object, or null while loading |
isLoading |
boolean |
true while the config is being fetched |
error |
StorefrontError | null |
Any fetch error, or null |
Example:
import { useCompanyConfig } from '@raulbellosom/atlas-sdk/react'
function StoreHeader() {
const { config, isLoading, error } = useCompanyConfig()
if (isLoading) return <header><span>Cargando...</span></header>
if (error) return <header><span>Error al cargar configuracion</span></header>
return (
<header>
<h1>{config?.storeName ?? 'Tienda'}</h1>
{config?.logoUrl && <img src={config.logoUrl} alt="Logo de la tienda" />}
</header>
)
}useBlueprints() + useHasModule(moduleKey)
Import: import { useBlueprints, useHasModule } from '@raulbellosom/atlas-sdk/react'
useBlueprints()
Signature: useBlueprints(): { blueprints, isLoading, error }
| Field | Type | Description |
|---|---|---|
blueprints |
Array<blueprint> |
All blueprints for installed modules. Empty array while loading |
isLoading |
boolean |
true while fetching |
error |
StorefrontError | null |
Any fetch error, or null |
Example:
import { useBlueprints } from '@raulbellosom/atlas-sdk/react'
function InstalledModules() {
const { blueprints, isLoading, error } = useBlueprints()
if (isLoading) return <p>Cargando modulos...</p>
if (error) return <p>Error al cargar modulos</p>
const moduleKeys = [...new Set(blueprints.map(bp => bp.module?.key).filter(Boolean))]
return (
<ul>
{moduleKeys.map(key => (
<li key={key}>{key}</li>
))}
</ul>
)
}useHasModule(moduleKey)
Signature: useHasModule(moduleKey: string): { hasModule, isLoading }
| Field | Type | Description |
|---|---|---|
hasModule |
boolean |
true if the given module is installed and enabled |
isLoading |
boolean |
true while checking |
Example:
import { useHasModule } from '@raulbellosom/atlas-sdk/react'
function BookingsSection() {
const { hasModule, isLoading } = useHasModule('custom.bookings')
if (isLoading) return null
if (!hasModule) return null
return (
<section>
<h2>Reservaciones</h2>
{/* booking UI here */}
</section>
)
}useProducts() + useProduct(id) + useCategories()
Import: import { useProducts, useProduct, useCategories } from '@raulbellosom/atlas-sdk/react'
useProducts(options?)
Signature: useProducts(options?: object): { data, isLoading, error }
options supports the same query parameters as sdk.catalog.products: page, limit, search, categoryId, sort, order. The options object is serialized with JSON.stringify and used as a useEffect dependency — passing an unstable object literal on every render will cause infinite fetching. Memoize with useMemo or useState if options change dynamically.
| Field | Type | Description |
|---|---|---|
data |
{ data: Array<product>, total, page, limit } | null |
API response envelope, or null while loading |
isLoading |
boolean |
true while fetching |
error |
StorefrontError | null |
Any fetch error, or null |
Example:
import { useState } from 'react'
import { useProducts } from '@raulbellosom/atlas-sdk/react'
function ProductGrid() {
const [page, setPage] = useState(1)
const { data, isLoading, error } = useProducts({ page, limit: 12 })
if (isLoading) return <p>Cargando productos...</p>
if (error) return <p>Error al cargar productos: {error.message}</p>
if (!data?.data?.length) return <p>No hay productos disponibles.</p>
return (
<div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{data.data.map((product) => (
<div key={product.id} style={{ border: '1px solid #ddd', padding: 16 }}>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
<div>
<button
disabled={page <= 1}
onClick={() => setPage(p => p - 1)}
>
Anterior
</button>
<span> Pagina {page} de {Math.ceil(data.total / 12)} </span>
<button
disabled={page >= Math.ceil(data.total / 12)}
onClick={() => setPage(p => p + 1)}
>
Siguiente
</button>
</div>
</div>
)
}useProduct(id)
Signature: useProduct(id: string | null): { data, isLoading, error }
Fetches a single product. If id is null or undefined, the hook does nothing (sets isLoading to false, data stays null).
| Field | Type | Description |
|---|---|---|
data |
product | null |
Product object, or null while loading or if id is null |
isLoading |
boolean |
true while fetching |
error |
StorefrontError | null |
Any fetch error, or null |
Example:
import { useProduct } from '@raulbellosom/atlas-sdk/react'
function ProductDetail({ productId }) {
const { data: product, isLoading, error } = useProduct(productId)
if (isLoading) return <p>Cargando producto...</p>
if (error?.code === 'NOT_FOUND') return <p>Producto no encontrado.</p>
if (error) return <p>Error al cargar producto: {error.message}</p>
if (!product) return null
return (
<article>
<h1>{product.name}</h1>
<p>${product.price}</p>
</article>
)
}useCategories(options?)
Signature: useCategories(options?: object): { data, isLoading, error }
Same behavior and caveats as useProducts but for categories.
| Field | Type | Description |
|---|---|---|
data |
{ data: Array<category>, total, page, limit } | null |
API response envelope |
isLoading |
boolean |
true while fetching |
error |
StorefrontError | null |
Any fetch error, or null |
Example:
import { useCategories } from '@raulbellosom/atlas-sdk/react'
function CategoryMenu() {
const { data, isLoading } = useCategories()
if (isLoading) return <nav><span>Cargando categorias...</span></nav>
return (
<nav>
{(data?.data ?? []).map((category) => (
<a key={category.id} href={`/categoria/${category.id}`}>
{category.name}
</a>
))}
</nav>
)
}useRequest()
Import: import { useRequest } from '@raulbellosom/atlas-sdk/react'
Signature: useRequest(): { execute, data, isLoading, error, reset }
Description: A stateful wrapper around sdk.request. Unlike the data hooks, useRequest does not run automatically — you call execute manually (e.g. on a button click or form submit).
Return value:
| Field | Type | Description |
|---|---|---|
execute |
async function(method, path, body?, options?) |
Calls sdk.request with the given arguments. Same signature as sdk.request. |
data |
any | null |
The last successful response, or null |
isLoading |
boolean |
true while a request is in progress |
error |
StorefrontError | null |
The last error, or null |
reset |
function() |
Clears data and error back to null |
Example:
import { useState } from 'react'
import { useRequest } from '@raulbellosom/atlas-sdk/react'
function CreateBookingForm() {
const { execute, isLoading, error, data, reset } = useRequest()
const [date, setDate] = useState('')
const [notes, setNotes] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await execute('POST', '/public/bookings', { date, notes })
} catch (err) {
// error is set on the hook
}
}
if (data) {
return (
<div>
<p>Reservacion creada con exito. ID: {data.data?.id}</p>
<button onClick={reset}>Crear otra reservacion</button>
</div>
)
}
return (
<form onSubmit={handleSubmit}>
{error && <p style={{ color: 'red' }}>{error.message}</p>}
<label>
Fecha
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
required
/>
</label>
<label>
Notas
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</label>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creando...' : 'Crear reservacion'}
</button>
</form>
)
}12. Auth patterns
a) Register flow with role selection
import { useState } from 'react'
import { useAuth } from '@raulbellosom/atlas-sdk/react'
import { StorefrontError } from '@raulbellosom/atlas-sdk'
function RegisterForm({ onSuccess }) {
const { register, isLoading, error } = useAuth()
const [form, setForm] = useState({ name: '', email: '', password: '', role: 'storefront_client' })
function setField(field) {
return (e) => setForm(prev => ({ ...prev, [field]: e.target.value }))
}
async function handleSubmit(e) {
e.preventDefault()
try {
await register({
name: form.name,
email: form.email,
password: form.password,
role: form.role,
})
onSuccess()
} catch (err) {
// error is set on the hook
}
}
return (
<form onSubmit={handleSubmit}>
<h2>Crear cuenta</h2>
{error && (
<p style={{ color: 'red' }}>
{error instanceof StorefrontError && error.code === 'VALIDATION_ERROR'
? 'Revisa los campos del formulario'
: error.message}
</p>
)}
<input placeholder="Nombre completo" value={form.name} onChange={setField('name')} required />
<input type="email" placeholder="Correo electronico" value={form.email} onChange={setField('email')} required />
<input type="password" placeholder="Contrasena" value={form.password} onChange={setField('password')} required />
<select value={form.role} onChange={setField('role')}>
<option value="storefront_client">Cliente</option>
<option value="storefront_vendor">Vendedor</option>
</select>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creando cuenta...' : 'Crear cuenta'}
</button>
</form>
)
}b) Login with redirect after success
import { useState } from 'react'
import { useAuth } from '@raulbellosom/atlas-sdk/react'
function LoginPage() {
const { login, isLoading, error } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await login({ email, password })
// Redirect to home after successful login
window.location.href = '/'
} catch (err) {
// error is set on the hook
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p style={{ color: 'red' }}>Correo o contrasena incorrectos</p>}
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Correo" required />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Contrasena" required />
<button type="submit" disabled={isLoading}>{isLoading ? 'Entrando...' : 'Entrar'}</button>
</form>
)
}c) Protected route pattern
This pattern works with any router. The example uses plain conditional rendering; adapt to React Router, TanStack Router, etc. as needed.
import { useSession } from '@raulbellosom/atlas-sdk/react'
import { useEffect } from 'react'
function ProtectedPage({ children }) {
const session = useSession()
useEffect(() => {
if (session === null) {
window.location.href = '/login'
}
}, [session])
if (session === null) {
return <p>Redirigiendo al inicio de sesion...</p>
}
return <>{children}</>
}
// Usage:
function AccountPage() {
return (
<ProtectedPage>
<h1>Mi cuenta</h1>
</ProtectedPage>
)
}d) Token auto-refresh
Token refresh is fully automatic. When any SDK method receives a 401 response, the SDK:
- Calls
supabase.auth.refreshSession()with the storedrefreshToken - The session adapter is updated automatically via Supabase's
onAuthStateChangeevent - Calls
onSessionChangewith the updated session (if configured) - Retries the original request with the new token
Concurrent 401s are deduplicated — only one refresh is made regardless of how many parallel requests fail. If the refresh itself fails (expired refresh token), supabase.auth.signOut() is called, the session is cleared, and onSessionChange(null) is triggered. No code is required in your application to handle refresh — only a redirect to login when the session becomes null.
e) Logout with localStorage cleanup
import { useAuth } from '@raulbellosom/atlas-sdk/react'
function LogoutButton() {
const { logout, isLoading } = useAuth()
async function handleLogout() {
await logout()
// onSessionChange(null) was already called, which removes the key if
// you wired it up as shown in the Quick Start. Manual cleanup:
localStorage.removeItem('sf_session')
window.location.href = '/login'
}
return (
<button onClick={handleLogout} disabled={isLoading}>
{isLoading ? 'Cerrando sesion...' : 'Cerrar sesion'}
</button>
)
}13. Common patterns for module-specific data
Custom ERP modules expose their data under /public/<moduleRoute>/.... Use sdk.request (plain JS) or useRequest (React) to interact with them. The pattern below uses a hypothetical custom.bookings module — apply the same pattern to any module.
Plain JS: read + create
// List all bookings for the current user
const response = await sdk.request('GET', '/public/bookings')
const bookings = response.data
// response.total, response.page, response.limit are also available if paginated
// Create a booking
const createResponse = await sdk.request('POST', '/public/bookings', {
serviceId: '01933b7e-0000-7000-8000-000000000003',
date: '2026-07-15',
time: '10:00',
})
const created = createResponse.data
// Get one booking
const booking = await sdk.request('GET', `/public/bookings/${created.id}`)
// Update a booking
await sdk.request('PATCH', `/public/bookings/${created.id}`, { status: 'CANCELLED' })React: read list with useRequest
import { useState, useEffect } from 'react'
import { useStorefront } from '@raulbellosom/atlas-sdk/react'
function BookingList() {
const sdk = useStorefront()
const [bookings, setBookings] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
setIsLoading(true)
sdk.request('GET', '/public/bookings')
.then(res => { if (!cancelled) setBookings(res.data ?? []) })
.catch(err => { if (!cancelled) setError(err) })
.finally(() => { if (!cancelled) setIsLoading(false) })
return () => { cancelled = true }
}, [sdk])
if (isLoading) return <p>Cargando reservaciones...</p>
if (error) return <p>Error al cargar reservaciones: {error.message}</p>
if (!bookings.length) return <p>No tienes reservaciones.</p>
return (
<ul>
{bookings.map(b => (
<li key={b.id}>
{b.date} — {b.status}
</li>
))}
</ul>
)
}React: imperative mutation with useRequest
import { useRequest } from '@raulbellosom/atlas-sdk/react'
function CancelBookingButton({ bookingId, onCancelled }) {
const { execute, isLoading, error } = useRequest()
async function handleCancel() {
try {
await execute('PATCH', `/public/bookings/${bookingId}`, { status: 'CANCELLED' })
onCancelled()
} catch (err) {
// error is set on the hook
}
}
return (
<div>
{error && <p style={{ color: 'red' }}>No se pudo cancelar: {error.message}</p>}
<button onClick={handleCancel} disabled={isLoading}>
{isLoading ? 'Cancelando...' : 'Cancelar reservacion'}
</button>
</div>
)
}14. Environment variables for Vite projects
There are two initialization patterns depending on how your frontend is deployed.
Pattern A — standalone Vite project (hardcoded URL)
Create a .env file in the root of your Vite project:
VITE_ERP_URL=https://erp.tudominio.mx
VITE_ERP_COMPANY=tu-empresa
VITE_SUPABASE_URL=https://supabase.tudominio.mx
VITE_SUPABASE_ANON_KEY=<anon-key>
Important: In Vite, only variables prefixed with VITE_ are exposed to the browser bundle. Never put secrets (service role keys, admin passwords) in VITE_ variables.
// src/sdk.js
import { createStorefrontClient } from '@raulbellosom/atlas-sdk'
export const sdk = createStorefrontClient({
baseUrl: import.meta.env.VITE_ERP_URL,
company: import.meta.env.VITE_ERP_COMPANY,
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
})Pattern B — deployed as Atlas Website dist (runtime config)
If you upload your built dist to Atlas Website (source_type=dist), Atlas automatically injects window.ATLAS_CONFIG into every HTML response at serve time. No .env file or hardcoded URLs needed — the config adapts to whichever Atlas instance is serving the site.
// src/sdk.js
import { createStorefrontClient } from '@raulbellosom/atlas-sdk'
const cfg = (typeof window !== 'undefined' && window.ATLAS_CONFIG) ? window.ATLAS_CONFIG : {}
export const sdk = createStorefrontClient({
baseUrl: cfg.apiUrl ?? import.meta.env.VITE_ERP_URL ?? '',
company: cfg.company ?? import.meta.env.VITE_ERP_COMPANY ?? '',
supabaseUrl: cfg.supabaseUrl ?? import.meta.env.VITE_SUPABASE_URL ?? '',
supabaseAnonKey: cfg.supabaseAnonKey ?? import.meta.env.VITE_SUPABASE_ANON_KEY ?? '',
})Fields available in window.ATLAS_CONFIG:
| Field | Description |
|---|---|
apiUrl |
Full URL of the Atlas ERP server — pass as baseUrl |
company |
Company slug — pass as company |
siteName |
Display name of the site configured in Atlas |
siteId |
Website site UUID used by analytics and forms |
analyticsMode |
off, anonymous, or consent_required |
turnstileSiteKey |
Public Cloudflare Turnstile site key, when configured |
stripePublishableKey |
Stripe publishable key, if configured on the site |
currency |
Site currency ('usd', 'mxn', etc.) |
supabaseUrl |
Supabase URL (advanced — for ERP session detection) |
supabaseAnonKey |
Supabase anon key (advanced) |
storageKey |
localStorage key for the ERP session (advanced) |
The two-fallback pattern (cfg.apiUrl ?? import.meta.env.VITE_ERP_URL) lets the same src/sdk.js work in both local development (env vars) and in production as an Atlas dist (injected config).
Then import the singleton wherever needed:
// src/main.jsx
import { sdk } from './sdk.js'
import { StorefrontProvider } from '@raulbellosom/atlas-sdk/react'
// ... wrap your app with <StorefrontProvider client={sdk}>Storefront capture v1 (SDK 0.3.0)
Public endpoints:
GET /public/storefront/v1/configPOST /public/storefront/v1/events/batchGET /public/storefront/v1/forms/:formIdPOST /public/storefront/v1/forms/:formId/submissions
Requests use X-Atlas-Company and optional X-Atlas-Site. Event batches accept at most 50 events and 64 KB.
Analytics
await sdk.analytics.start()
sdk.analytics.setConsent('granted')
sdk.analytics.page({ section: 'pricing' })
sdk.analytics.track('pricing_cta', { placement: 'hero' })
await sdk.analytics.flush()Available methods: start, page, track, setConsent, getConsent, flush, and stop.
- DNT always disables analytics.
consent_requiredstores nothing until consent is granted.- Denying consent clears queued events and local analytics identifiers.
- Sessions rotate after 30 minutes of inactivity.
- Automatic capture is limited to pageviews, visible time, form lifecycle, and tagged elements.
- Event properties reject PII, form values, nested objects, and more than 20 scalar keys.
<button
data-atlas-event="pricing_cta"
data-atlas-label="Plan profesional"
data-atlas-placement="hero"
>
Cotizar
</button>Public forms
const definition = await sdk.forms.get(formId)
const result = await sdk.forms.submit(formId, values, {
idempotencyKey: crypto.randomUUID(),
turnstileToken,
honeypot: '',
})sdk.forms.get(formId) returns the form definition with all fields:
{
"data": {
"id": "019...",
"name": "Formulario de contacto",
"submitLabel": "Enviar",
"wizardMode": false,
"fields": [
{
"id": "019...", "name": "nombre", "label": "Nombre",
"fieldType": "text", "required": true, "options": null,
"sortOrder": 0, "stepNumber": 1, "stepTitle": null
}
]
}
}Supported field types:
fieldType |
Value format | Notes |
|---|---|---|
text |
"string" |
Single-line text input |
email |
"string" |
Email address |
tel / phone |
"string" |
Phone number |
textarea |
"string" |
Multi-line text |
number |
"string" |
Numeric value as string |
date |
"YYYY-MM-DD" |
ISO date string |
checkbox |
"true" | "false" |
Boolean as string |
select |
"option_value" |
One of the options array values |
radio |
"option_value" |
One of the options array values |
chip_multi |
"opt1,opt2" |
Comma-joined multi-selection; options is string[] |
card_select |
"option_value" |
Single selection; options is {value, label, description}[] |
Wizard mode: When wizardMode: true, each field has stepNumber (integer) and stepTitle (string or null). Group fields by stepNumber and present them as steps in your UI. The submit endpoint is the same regardless of wizard mode — send all field values in a single values object.
Submission values are sent only to the form endpoint. Analytics receives form and submission identifiers, never field values.
React exports: useAnalytics, usePageView, and usePublicForm.
Plain HTML can use the automatically loaded IIFE:
<div id="contact-form"></div>
<script>
window.AtlasERP.analytics.setConsent('granted')
window.AtlasERP.renderForm('#contact-form', {
formId: '01900000-0000-7000-8000-000000000003',
theme: 'auto',
onSuccess: function (result) {
console.log(result.submissionId)
}
})
</script>The legacy endpoint POST /public/website/forms/:formId/submit is deprecated in 0.3.0. Removal is planned for 0.4.0, not before September 30, 2026.
15. Roles system
The SDK uses a two-tier storefront role system for public-facing users. These roles are separate from the internal ERP admin roles.
Built-in storefront roles
| Role | Description | File size limit | Allowed file types |
|---|---|---|---|
storefront_client |
End customer. Can browse the catalog, place orders, upload profile images, and submit reviews. | 5 MB | Images only (image/*) |
storefront_vendor |
Seller or partner. Can manage their own product listings and upload product media or documents. | 100 MB | All file types |
Registrable roles
Not all roles are available for self-registration. The platform admin configures which roles users can select when calling sdk.auth.register. If a role is not in the registrable list, the API returns a VALIDATION_ERROR.
You cannot self-register as admin or any internal ERP role. Attempting to pass an admin role to register() will be rejected by the server with a 422 error.
Checking the current user's role
const session = sdk.auth.getSession()
if (session?.user?.role === 'storefront_vendor') {
// show vendor-specific features
}Or in React:
import { useSession } from '@raulbellosom/atlas-sdk/react'
function VendorOnlyButton() {
const session = useSession()
if (session?.user?.role !== 'storefront_vendor') return null
return <button>Panel de vendedor</button>
}16. Deploying to Atlas Website (source_type=dist)
Atlas Website sup