Adhara Integration Overview

Local JSON by default, a real CMS backend when you connect one — and why every feature switches independently, not all-or-nothing.

Adhara is a real, hosted headless CMS/CRM/commerce platform. This project never requires it — every feature works against local JSON files out of the box — but almost every feature can switch to reading (and sometimes writing) real Adhara data once you connect one, without touching any calling code.

Why per-feature, not all-or-nothing

A site owner might want events managed by a real content team in Adhara while the customer portal stays local, or might want the shop on Adhara Commerce but the blog still local. So instead of one global “use Adhara” flag, each feature has its own independent env var:

GRAVITY_BLOG_BACKEND=local|adhara
GRAVITY_EVENTS_BACKEND=local|adhara
GRAVITY_PORTAL_BACKEND=local|adhara
GRAVITY_SCHEDULING_BACKEND=local|adhara
GRAVITY_LINKS_BACKEND=local|adhara
GRAVITY_SHOP_BACKEND=local|adhara
GRAVITY_GALLERY_BACKEND=local|adhara

Leave any of them unset and it auto-detects — once the Adhara config that specific feature actually needs is present (a workspace slug for public reads, an API key for authenticated ones, the SDK installed for writes), it switches over automatically. Set it explicitly to force one way or the other. /admin shows a live “Feature backends” panel with the current resolution for every feature.

All seven go through one shared resolver — resolveFeatureBackend() / resolveFeatureBackendDetailed() in src/lib/featureBackend.ts:

export function resolveFeatureBackendDetailed(envVar: string, autoDetect: () => boolean) {
  const raw = (process.env[envVar] || '').trim().toLowerCase();
  if (raw === 'local') return { backend: 'local', forced: true };
  if (raw === 'adhara') return { backend: 'adhara', forced: true };
  return { backend: autoDetect() ? 'adhara' : 'local', forced: false };
}

If you’re wiring up a new switchable feature, reuse this resolver — don’t hand-roll another (process.env.X || '').trim().toLowerCase() check.

Every feature resolves and behaves differently — by design

The resolver only answers “local or adhara, forced or auto.” What each feature does with that answer is deliberately different, matching what’s actually possible on the real backend:

Feature Auto-detects on Local ↔ Adhara relationship
Blog (writes) SDK installed + API key + workspace ID Reads always merge local ∪ Adhara regardless of this var — only writes (create/update/publish) switch
Events Workspace slug (public endpoint, no key needed) Hard switch: local never calls Adhara; adhara merges local ∪ Adhara (local wins on a collision)
Portal API key + workspace Hard switch, one provider active at a time — see Customer Portal
Scheduling API key + workspace ID + workspace slug No local content store at all — local is a fixed, labeled demo (booking refused); real booking needs Adhara’s availability engine
Links Workspace slug Hard switch, but a whole-page swap, not a merge — bio/socials/links all come from one backend at once
Shop SDK installed + workspace slug Content split like blog/events; checkout has three real paths — see Commerce
Gallery API key + workspace ID (workspace slug alone isn’t enough) Content split; local itself has two sub-sources (disk vs. cloud bucket) — see Media Gallery

Three features are not part of this toggle family at all, because their real shape doesn’t fit a local/Adhara switch:

  • Podcast (/podcast) — episode data always comes from parsing a real RSS/Atom feed; there’s no local content to switch away from. Only the feed URL is resolved (env var, or an Adhara /links entry tagged category: "podcast").
  • Contact & newsletter — “always local, additively synced”: every submission saves locally first, unconditionally, and Adhara sync (a real form / a CRM lead) activates automatically once configured, but never gates the local save. There’s deliberately no GRAVITY_CONTACT_BACKEND var — that would imply local is skippable, which it isn’t.

See Features for what each feature actually does, and the SDK section for how the Adhara SDK itself is treated.