Content Storage Architecture — JCR vs EDS Content Bus
Content storage in AEM and EDS. Covers JCR, da.live, the EDS content bus, the preview and publish pipeline, asset storage, and the path from authoring to browser render.
Content Objective
This chapter covers:
- How AEM stores content in the JCR (Oak) and what that means for developers
- How EDS stores content and why it stores it differently
- The complete content flow from author action to browser render in both systems
- How the Preview/Publish pipeline works in EDS
- Where assets (images, documents) are stored in EDS
- The implications of each storage model for performance, scalability, and development
- What changes when you use xwalk (UE) vs Document Authoring in terms of content storage
The question "where does content go?" sounds simple. The answer reveals the entire architectural philosophy of each system.
How AEM Stores Content: The JCR
What is the JCR
The Java Content Repository (JCR) is a hierarchical content storage API (JSR-283). AEM uses Apache Jackrabbit Oak as its JCR implementation.
The JCR stores everything in a tree of nodes. Every piece of content — pages, components, configurations, user data, workflow instances, audit logs — is a node or property in this tree.
/content/
├── mysite/
│ ├── en/
│ │ ├── home/
│ │ │ ├── jcr:content/ ← Page properties
│ │ │ │ ├── jcr:title = "Home Page"
│ │ │ │ ├── cq:template = "/conf/mysite/templates/home"
│ │ │ │ └── root/
│ │ │ │ ├── hero/
│ │ │ │ │ ├── jcr:primaryType = "nt:unstructured"
│ │ │ │ │ ├── size = "tall"
│ │ │ │ │ └── title = "Welcome"
│ │ │ │ └── cards/
│ │ │ │ └── ...
/apps/
├── mysite/ ← Application code
├── components/
└── templates/
/conf/
├── mysite/ ← Configuration
│ └── settings/
│ └── wcm/
│ └── templates/
/var/
├── workflow/ ← Workflow instances
└── audit/ ← Audit trail
/dam/
└── content/dam/ ← Digital assets
JCR Characteristics
Persistence: Content is stored on disk in a segmented node store (Oak's SegmentNodeStore). The actual storage is in binary files (tar/segment format), not a traditional relational database.
Hierarchy: Every piece of content has a path. Content is addressed by path: /content/mysite/en/home/jcr:content.
Node Types: Every node has a type (nt:unstructured, cq:Page, dam:Asset, etc.). Types define which properties are allowed and which child nodes are valid.
Namespaces: Property names use namespaces (jcr:title, cq:template, sling:resourceType). Each namespace (jcr:, cq:, sling:) has a specific meaning in the AEM framework.
Binary Storage: Binary data (images, PDFs, videos) is stored in the DataStore — a blob storage layer separate from the node store, but referenced by nodes.
Versioning: The JCR has built-in content versioning. Every time a page is activated (published), a version is created. You can roll back to any previous version.
Access Control: ACLs (Access Control Lists) are stored as nodes (rep:policy children) directly in the content tree. Permissions are enforced at the repository level.
How a Page Render Works in AEM
Browser requests /content/mysite/en/home.html
↓
Apache Dispatcher checks cache
↓ (cache miss)
AEM Publish instance receives request
↓
Sling URL Decomposition:
Resource: /content/mysite/en/home
Extension: .html
↓
Sling Resource Resolution:
Finds node at /content/mysite/en/home/jcr:content
Reads sling:resourceType = "mysite/components/page"
↓
HTL rendering:
Loads /apps/mysite/components/page/page.html
Instantiates Sling Model (PageModel.java)
Model reads JCR properties via ValueMap
HTL outputs HTML using model data
↓
Component children rendered recursively
↓
Complete HTML returned
↓
Apache Dispatcher caches the HTML
↓
Response sent to browser
The entire rendering pipeline runs on the server. No JCR content ever goes to the browser raw — the server transforms it into HTML.
How EDS Stores Content
EDS has two content storage backends, depending on which authoring approach you use.
Storage Backend 1: da.live (Document Authoring)
When authors use Document Authoring, content is stored in da.live — Adobe's cloud content storage service built specifically for EDS.
Under the hood, da.live stores content as:
- Structured HTML files (for pages authored in the DA editor)
- Original document files (when content is authored in SharePoint or Google Drive)
- Binary assets (images, documents) in Adobe's blob storage
Content path: Each page is stored at a path that mirrors the URL structure:
da.live stores:
/mysite/en/home ← the home page document
/mysite/en/about ← the about page document
/mysite/en/news/article-1 ← a news article
Important: da.live content is NOT in JCR. It is not in AEM's node store. It is a completely separate storage system.
Storage Backend 2: AEM JCR (Universal Editor / xwalk)
When authors use Universal Editor (xwalk), content IS stored in AEM JCR — the same storage as traditional AEM.
Pages created in UE are stored at:
/content/{site}/{lang}/{path}
The bin/franklin.delivery servlet on the AEM author instance reads this JCR content and converts it to EDS-compatible HTML when the Edge Network requests it.
This is why xwalk projects have a fstab.yaml pointing to the AEM author instance — EDS needs to fetch content from AEM's JCR via the delivery servlet.
The Content Flow: DA Authoring
Author edits in da.live editor
↓
Content saved to da.live cloud storage (as HTML)
↓ (author clicks Preview)
da.live triggers: EDS Preview API
↓
EDS Edge Network fetches from da.live storage
Converts document HTML to block HTML
Stores in Content Bus (edge cache)
↓
Preview URL (.aem.page) now serves new content
↓ (author clicks Publish)
EDS Edge Network promotes content to Live tier
↓
Live URL (.aem.live) now serves new content
↓ (user visits the page)
Browser receives HTML from edge node
Browser loads block JS/CSS from GitHub (via aem-code-sync)
Block JS decorates the HTML
Page renders
The Content Bus
The Content Bus is EDS's internal content cache layer. It sits between the authoring system (da.live or AEM) and the edge delivery nodes.
The Content Bus:
- Stores the HTML representation of each page
- Is populated when Preview is clicked (not on save — this is intentional)
- Has two tiers: Preview (
.aem.page) and Live (.aem.live) - Is invalidated automatically when Publish is clicked
Why content only updates on Preview click, not on save: The Content Bus is a production cache. If it updated on every save, partially completed edits would immediately appear on the preview site. The explicit Preview action is the author's declaration that "this content is ready to be seen."
The Content Flow: xwalk (Universal Editor)
Author edits in Universal Editor
↓
Content saved to AEM JCR
(same as traditional AEM page authoring)
↓
Optional: AEM Workflow runs (approval, legal review)
↓ (author clicks Preview in Sidekick)
Sidekick calls EDS Preview API:
POST /api/v1/web/helix-services/content-proxy/.../preview/{path}
↓
EDS Edge Network fetches from:
https://author-pXXX.adobeaemcloud.com/bin/franklin.delivery/{owner}/{repo}/main/{path}.html
↓
bin/franklin.delivery reads AEM JCR content
Converts JCR component structure to EDS block HTML table format
↓
EDS Edge stores in Content Bus (Preview tier)
↓
Preview URL serves new content
↓ (author clicks Publish in Sidekick)
EDS promotes from Preview tier to Live tier
Live URL serves new content
What bin/franklin.delivery Does
The bin/franklin.delivery servlet is a critical bridge between AEM JCR and EDS. It:
- Reads the page node from JCR (
/content/mysite/...) - Iterates through the page's component tree
- Converts each component into an HTML block table:
<!-- Component in JCR: hero-banner with size="tall", title="Welcome" --> <!-- Converted to: --> <table> <tr><td>hero-banner</td></tr> <tr><td>size</td><td>tall</td></tr> <tr><td>title</td><td>Welcome</td></tr> </table> - Returns complete HTML that EDS can process
This conversion is what allows UE/xwalk content (stored in JCR) to be delivered by the EDS edge network (which expects HTML block tables).
Asset Storage
Assets in AEM
Assets are stored in the DAM (Digital Asset Management):
- Binary files in the DataStore (file system or cloud blob storage)
- Metadata in JCR at
/content/dam/... - Multiple renditions generated by DAM workflows
- Full asset management: search, tagging, collections, metadata schemas
Assets in EDS
Assets are stored in:
For DA authoring: da.live asset storage. When an author inserts an image in the DA editor, it is uploaded to da.live and referenced by URL.
For xwalk authoring: AEM DAM. Images are stored in DAM at /content/dam/... exactly as in traditional AEM.
How EDS delivers images: EDS uses the Media Bus — a smart image delivery pipeline that:
- Accepts original images (any format, any resolution)
- Generates optimized renditions on-demand (WebP, specific dimensions)
- Caches renditions at the edge
- Returns
<picture>elements with multiple<source>elements for different breakpoints
<!-- EDS-generated responsive image markup -->
<picture>
<source type="image/webp" srcset="/media/hero.jpg?width=2000&format=webply&optimize=medium 2000w,
/media/hero.jpg?width=750&format=webply&optimize=medium 750w" />
<source srcset="/media/hero.jpg?width=2000&optimize=medium 2000w,
/media/hero.jpg?width=750&optimize=medium 750w" />
<img src="/media/hero.jpg?width=750&format=webply&optimize=medium"
width="750" height="500"
loading="eager" alt="Hero image" />
</picture>
This is generated automatically by EDS's image processing pipeline. You do not write this HTML. It is a significant advantage — automatic responsive images with WebP format at no developer effort.
Content Versioning
In AEM
AEM JCR has built-in content versioning:
- Every activation creates a version
- Versions are browseable in the Timeline panel
- Authors can compare versions (diff)
- Authors can restore any previous version
- Versions are stored in
/jcr:system/jcr:versionStorage/
In EDS
EDS does not have native content versioning at the CMS level.
For DA authoring: da.live maintains an internal history, but the UI for browsing/restoring versions is limited compared to AEM's versioning.
For xwalk authoring: Since content is in AEM JCR, AEM's full versioning capabilities are available. This is one advantage of the xwalk path.
Git as version control for code: All code changes (block JS, CSS, JSON models) are versioned in Git. You can roll back code changes with git revert.
Content Replication
In AEM
Traditional AEM uses replication agents to push content from Author to Publish:
- Replication queue
- Multiple publish instances
- Replication filters (what to replicate)
- Reverse replication (user-generated content from Publish to Author)
- Transport-level encryption (HTTPS, mTLS)
In EDS
There is no replication. The Preview → Publish workflow replaces replication:
| AEM Concept | EDS Equivalent |
|---|---|
| Author instance | AEM author (xwalk) or da.live (DA) |
| Publish instance | EDS Content Bus (Live tier) |
| Dispatcher cache | EDS CDN edge cache |
| Activate (replicate) | Sidekick Publish action |
| Replication agent flush | Automatic CDN invalidation on Publish |
| Staged publishing | Preview tier (.aem.page) |
The key difference: AEM's replication is author-pushed (author triggers replication to publish). EDS is edge-pull (the edge network fetches from the authoring source when Preview is clicked).
The Architecture Side by Side
AEM Architecture
+------------------+
| Author Instance|
| (AEM + JCR) |
+--------+---------+
|
Replication
|
+-----------+------------+
| Publish Instances |
| (AEM + JCR replica) |
+------+-------+----------+
| |
+------+ +------+
| |
+--------+--------+ +--------+--------+
| Dispatcher | | Dispatcher |
| (Cache + CDN) | | (Cache + CDN) |
+-----------------+ +-----------------+
|
Browser
EDS Architecture
+------------------+ +------------------+
| AEM Author | | da.live |
| (xwalk content | | (DA content |
| in JCR) | | storage) |
+--------+---------+ +--------+---------+
| |
bin/franklin.delivery da.live API
| |
+--------+-----------------------------+--------+
| EDS Content Bus |
| (Preview tier .aem.page / Live .aem.live) |
+---------------------------+--------------------+
|
+-----------+-----------+
| EDS CDN Edge Nodes |
| (Global, auto-scaling) |
+-----------+-----------+
|
Browser
(loads block JS/CSS from GitHub)
What This Means for Developers
Data Access Patterns
| In AEM (Sling Model) | In EDS (Block JS) |
|---|---|
resource.adaptTo(ValueMap.class).get("title", String.class) | props.title?.textContent.trim() |
resourceResolver.getResource("/content/data") | await fetch('/data/query-index.json') |
queryBuilder.createQuery(queryMap, session) | Client-side array filter on JSON index |
session.getNode("/content/dam/images") | Not available |
OSGi @Reference to service | import utility function |
| JCR observation for real-time updates | Not available |
The Stateless Delivery Model
EDS delivery is stateless. There is no session, no request context, no user authentication at the delivery layer.
If your application requires:
- User authentication: Handle via JavaScript (Adobe IMS, Auth0, etc.) after initial page load
- Personalized content: Use the experimentation plugin or Adobe Target, loaded in delayed.js
- User-specific data: Fetch from an external API after authentication
- Shopping cart / form state: External service (not AEM, not EDS)
EDS delivers the same HTML to every user. Personalization happens client-side, after delivery.
Structured Content: Content Fragments vs EDS
AEM Content Fragments
AEM Content Fragments provide structured content modeling:
- Define a schema (Content Fragment Model) with typed fields
- Authors fill in structured data
- Fragments are referenced by pages (not embedded)
- GraphQL API for headless consumption
- Variations for different contexts
- Versioning and approval workflow
Content Fragments are AEM's answer to headless CMS. They are powerful but require the full AEM stack.
EDS Approach to Structured Content
EDS does not have Content Fragments. Structured data is handled by:
-
JSON Sheets (Spreadsheet-based data): A SharePoint or Google Sheets spreadsheet is available as a JSON endpoint. This works for tabular data (pricing tables, product specifications, office locations).
-
Query API: Page metadata as structured data. If you store data in page metadata, the query API provides it as JSON.
-
External Headless CMS: For truly structured content, EDS blocks can
fetch()from any external API — AEM Content Fragments API, Contentful, Hygraph, etc. -
Authored block tables: Complex structured data can be authored as a block with many rows, each row representing one item. The block JS reads the table and renders the data.
Key Takeaways
- DA content is stored in
da.livecloud storage — NOT in JCR, NOT in a traditional database - xwalk (UE) content IS stored in AEM JCR — same as traditional AEM, delivered via
bin/franklin.delivery - The Content Bus is EDS's delivery cache — populated by Preview action, promoted by Publish action
- Preview ≠ Save: Content only enters the Content Bus when Preview is explicitly clicked
- Assets flow through the Media Bus — automatic WebP conversion and responsive image generation
- No replication agents: Preview/Publish replaces AEM's activation/replication model
- Stateless delivery: EDS delivers the same HTML to every user — personalization is client-side
- No Content Fragments in EDS: Use JSON sheets, Query API, or external headless APIs
- xwalk preserves more storage capabilities: Versioning, workflow, DAM integration — because content stays in JCR
- The fundamental difference: AEM assembles HTML on the server from JCR; EDS delivers raw content HTML from the edge and the browser assembles the final UI via block JS
Next Steps
In the final chapter of this series, we bring everything together: What Is Not Feasible in EDS — And What to Do Instead.
We will create a definitive reference for architecture decisions: when to use EDS, when to stay with AEM, and the hybrid patterns that combine both.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.