Core concepts.

sparx is a modular content and commerce operating system. Five ideas shape every endpoint in these docs — understand them once and the rest of the API follows.

Updated 2026-06-058 min read

Tenants & isolation

Every account on sparx is a tenant. A tenant owns all of its data — sites, products, customers, content, emails — and is completely isolated from every other tenant. Authentication is handled by self-hosted Better Auth, whose organizations map one-to-one to sparx tenants.

Isolation is not an application convenience you have to remember to apply — it’s enforced at the database. Every tenant-scoped table carries a tenant_id, and PostgreSQL Row-Level Security policies are the backstop: even a buggy query can only ever see the current tenant’s rows. Your API key carries a tenant context, and the database refuses to return anything outside it.

What this means for youYou never pass a tenant_id in API calls and you can’t reach another tenant’s data by guessing ids. The key is the tenant scope.

Modules

sparx is one platform made of independently-activated modules. A tenant turns on only what it uses and pays only for that — a CMS-only publisher, a CRM-only team, and a full B2B distributor are all equally first-class. Selling is one capability, never the assumption.

ModuleWhat it does
builderThe visual site builder — pages, layouts, and reusable components as node trees.
commerceCatalog, variants, cart, checkout, and the storefront.
cmsContent types, entries, revisions, and publishing.
crmCustomers, B2B accounts, deals, and orders.
invoicingQuotes, estimates, invoices, and payments — one document engine.
emailBroadcasts, automations, and transactional sends.
b2bWholesale accounts, payment terms, price lists, and quoting.
dropshipSupplier integration and dropship fulfillment.
aiThe MCP server — AI agents reading and writing live tenant data.

Modules are feature-flagged, not separately deployed. A disabled module stores no rows, runs no workers, and its endpoints return a clear error rather than partial behavior:

{ "success": false, "error": { "code": "module_disabled", "message": "The commerce module is not active for this tenant." } }
The Settings → Modules screen — each module (Builder, Commerce, CMS, CRM, Email active; B2B, Dropship, AI inactive) with an Activate or Deactivate button.
Settings → Modules — activate only what you use. Disabled modules return 404, run no consumers, and store no rows.

API-first

Every feature exists as an API endpoint before it exists as a screen. The dashboard at app.sparx.works is just one consumer of the same surface your code calls — anything the UI can do, you can do. The API is exposed two ways from one schema:

  • REST at api.sparx.works/v1 — resource-oriented, the default for most integrations and webhooks.
  • GraphQL — the same data, for fetching a deep object graph in one round trip.
  • MCP at mcp.sparx.works — the API as tools an AI agent can call directly.

Every REST response shares one envelope, so success, pagination, and errors are uniform:

// Success
{ "success": true, "data": { /* the resource */ } }

// Success with pagination
{ "success": true, "data": [ /* … */ ], "meta": { "next_cursor": "…", "total": 128 } }

// Error
{ "success": false, "error": { "code": "module_disabled", "message": "…" } }

Successful responses are { success: true, data } (with an optional meta for pagination); failures are { success: false, error } with a machine-readable error.code.

Events

sparx never inlines side effects in a request handler. When something happens — an order is paid, a content entry is published — the handler writes its data and publishes an event. Workers consume those events asynchronously: rendering and sending email, reindexing search, revalidating caches.

This keeps writes fast and the system loosely coupled — and it’s the same stream you can subscribe to. See Webhooks & events for the catalog, the signed delivery model, and how to receive events in your own app.

Practically: don’t poll for changes. Subscribe to the event you care about and let sparx push it to you with retries.

One data layer

Modules share a single database, not a constellation of disconnected services. A customer created in the CRM is the same customer Commerce attaches an order to and Email sends a broadcast to — there are no parallel records to reconcile. The CRM owns the customer spine (customers and b2b_accounts); other modules reference it.

The flip side of one shared layer is that isolation has to be ironclad — which is exactly why Row-Level Security sits underneath everything. Your data is yours: it’s never blended with another tenant’s, and it’s exportable.

The surfaces

Where everything lives:

HostWhat it serves
api.sparx.worksThe REST & GraphQL API.
mcp.sparx.worksThe MCP server for AI agents.
app.sparx.worksThe tenant dashboard.
<tenant>.sparx.zoneTenant storefronts (and custom domains).
sparx.worksThe marketing site and these docs.

Next, get a key and make your first authenticated call — Authentication.

Was this page helpful?