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/linksentry taggedcategory: "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_BACKENDvar — 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.